<?php
declare(strict_types=1);

if (session_status() !== PHP_SESSION_ACTIVE) {
    session_start();
}

$isPreview = (int)($_GET['preview'] ?? 0) === 1;

if ($isPreview) {
    $teacherLoggedIn = !empty($_SESSION['user_id'])
        && in_array((string)($_SESSION['user_role'] ?? ''), ['teacher', 'admin'], true);

    if (!$teacherLoggedIn) {
        $_SESSION['teacher_return_url'] = (string)($_SERVER['REQUEST_URI'] ?? '');
        header('Location: ../teacher/login.php');
        exit;
    }
} elseif (empty($_SESSION['student_id'])) {
    $_SESSION['student_return_url'] = (string)($_SERVER['REQUEST_URI'] ?? '');
    header('Location: student-login.php');
    exit;
}

require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../includes/sentence_image_resolver.php';
require_once __DIR__ . '/student-runtime/engines/pronunciation_feedback_engine.php';


$pdo = getDbConnection();

$bookSlug = trim((string)($_GET['book'] ?? ''));
$levelSlug = trim((string)($_GET['level'] ?? ''));
$levelId = max(0, (int)($_GET['level_id'] ?? 0));
$unitId = max(0, (int)($_GET['unit_id'] ?? ($_GET['id'] ?? 0)));
$lessonId = max(0, (int)($_GET['lesson_id'] ?? 0));
$activityId = max(0, (int)($_GET['activity_id'] ?? ($_GET['activity'] ?? 0)));
if ($bookSlug === '' || $levelId <= 0 || $unitId <= 0 || $lessonId <= 0 || $activityId <= 0) {
    header('Location: student-learning.php');
    exit;
}

function slrColumns(PDO $pdo, string $table): array
{
    $stmt = $pdo->prepare(
        "SELECT COUNT(*) FROM information_schema.TABLES
         WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?"
    );
    $stmt->execute([$table]);

    if ((int)$stmt->fetchColumn() === 0) {
        return [];
    }

    $rows = $pdo->query("SHOW COLUMNS FROM `{$table}`")->fetchAll(PDO::FETCH_ASSOC);
    return array_column($rows, null, 'Field');
}

function slrAssetUrl(string $path): string
{
    $path = trim($path);

    if ($path === '') {
        return '';
    }

    if (
        str_starts_with($path, 'http://')
        || str_starts_with($path, 'https://')
        || str_starts_with($path, 'data:')
        || str_starts_with($path, '/')
    ) {
        return $path;
    }

    return '/EnglishSpeak/' . ltrim($path, './');
}


function slrFirstColumn(array $columns, array $candidates): ?string
{
    foreach ($candidates as $candidate) {
        if (isset($columns[$candidate])) {
            return $candidate;
        }
    }

    return null;
}

function slrNormalizeMatchText(string $text): string
{
    $text = mb_strtolower(trim($text));
    $text = preg_replace('/[^\p{L}\p{N}\s\'-]+/u', ' ', $text) ?? $text;
    $text = preg_replace('/\s+/u', ' ', $text) ?? $text;

    return trim($text);
}

function slrContainsWholeWord(string $sentence, string $word): bool
{
    $sentence = slrNormalizeMatchText($sentence);
    $word = slrNormalizeMatchText($word);

    if ($sentence === '' || $word === '') {
        return false;
    }

    return preg_match(
        '/(?:^|\s)' . preg_quote($word, '/') . '(?:$|\s)/u',
        $sentence
    ) === 1;
}

function slrSentenceMatchesWord(string $sentence, string $word): bool
{
    if (slrContainsWholeWord($sentence, $word)) {
        return true;
    }

    $normalizedWord = slrNormalizeMatchText($word);

    if ($normalizedWord === '') {
        return false;
    }

    $variants = [];

    // Basic plural support for primary-school vocabulary:
    // apple <-> apples, baby <-> babies, bus <-> buses.
    if (str_ends_with($normalizedWord, 'y') && mb_strlen($normalizedWord) > 1) {
        $variants[] = mb_substr($normalizedWord, 0, -1) . 'ies';
    } elseif (
        str_ends_with($normalizedWord, 's')
        || str_ends_with($normalizedWord, 'x')
        || str_ends_with($normalizedWord, 'z')
        || str_ends_with($normalizedWord, 'ch')
        || str_ends_with($normalizedWord, 'sh')
    ) {
        $variants[] = $normalizedWord . 'es';
    } else {
        $variants[] = $normalizedWord . 's';
    }

    foreach ($variants as $variant) {
        if (slrContainsWholeWord($sentence, $variant)) {
            return true;
        }
    }

    return false;
}

function slrIsMeaningfulImageWord(string $word): bool
{
    $word = slrNormalizeMatchText($word);

    if ($word === '' || mb_strlen($word) < 3) {
        return false;
    }

    $ignoredWords = [
        'the', 'this', 'that', 'these', 'those',
        'and', 'or', 'but', 'with', 'from', 'for', 'into', 'onto',
        'are', 'was', 'were', 'has', 'have', 'had',
        'like', 'likes', 'liked',
        'can', 'could', 'will', 'would', 'should',
        'you', 'your', 'they', 'their', 'them',
        'she', 'her', 'him', 'his', 'our', 'ours',
        'not', 'yes', 'no',
    ];

    return !in_array($word, $ignoredWords, true);
}

function slrImageKeywordCandidates(string $sentence): array
{
    $normalized = slrNormalizeMatchText($sentence);

    if ($normalized === '') {
        return [];
    }

    $ignoredWords = [
        'i', 'a', 'an', 'the',
        'this', 'that', 'these', 'those',
        'is', 'am', 'are', 'was', 'were', 'be', 'been',
        'and', 'or', 'but',
        'with', 'from', 'for', 'into', 'onto', 'to', 'of', 'in', 'on', 'at',
        'have', 'has', 'had',
        'like', 'likes', 'liked',
        'can', 'could', 'will', 'would', 'should',
        'you', 'your', 'he', 'she', 'it', 'we', 'they',
        'his', 'her', 'our', 'their', 'them',
        'not', 'yes', 'no',
    ];

    $candidates = [];

    foreach (preg_split('/\s+/u', $normalized) ?: [] as $token) {
        $token = trim($token);

        if (
            $token === ''
            || mb_strlen($token) < 3
            || in_array($token, $ignoredWords, true)
        ) {
            continue;
        }

        $candidates[$token] = true;

        // Safe singular forms: apples -> apple, babies -> baby.
        if (str_ends_with($token, 'ies') && mb_strlen($token) > 3) {
            $candidates[mb_substr($token, 0, -3) . 'y'] = true;
        } elseif (str_ends_with($token, 'es') && mb_strlen($token) > 3) {
            $candidates[mb_substr($token, 0, -2)] = true;
            $candidates[mb_substr($token, 0, -1)] = true;
        } elseif (str_ends_with($token, 's') && mb_strlen($token) > 3) {
            $candidates[mb_substr($token, 0, -1)] = true;
        }
    }

    return array_keys($candidates);
}

$activityColumns = slrColumns($pdo, 'activities');
$sentenceSetColumns = slrColumns($pdo, 'sentence_sets');
$sentenceColumns = slrColumns($pdo, 'sentences');

$relationTable = '';
foreach (['sentence_set_sentences', 'sentence_set_items'] as $candidateTable) {
    if (slrColumns($pdo, $candidateTable) !== []) {
        $relationTable = $candidateTable;
        break;
    }
}
$relationColumns = $relationTable !== ''
    ? slrColumns($pdo, $relationTable)
    : [];

if ($activityColumns === []) {
    http_response_code(500);
    exit('Thiếu bảng dữ liệu Speaking Sentences.');
}

$activityTitleColumn = isset($activityColumns['title'])
    ? 'title'
    : (isset($activityColumns['name']) ? 'name' : null);

if ($activityTitleColumn === null) {
    http_response_code(500);
    exit('Cấu trúc Activity chưa đầy đủ.');
}

$activityStatusWhere = isset($activityColumns['status'])
    ? "AND a.status = 'active'"
    : '';

$pronunciationFocusSelect = isset($activityColumns['pronunciation_focus'])
    ? "a.pronunciation_focus AS pronunciation_focus,"
    : "'none' AS pronunciation_focus,";

$sentenceSetIdSelect = isset($activityColumns['sentence_set_id'])
    ? 'a.sentence_set_id AS sentence_set_id,'
    : '0 AS sentence_set_id,';

$contextStmt = $pdo->prepare(
    "SELECT
        a.id AS activity_id,
        {$sentenceSetIdSelect}
        a.xp_reward AS xp_reward,
        {$pronunciationFocusSelect}
        a.`{$activityTitleColumn}` AS activity_title,
        le.id AS lesson_id,
        le.lesson_number,
        le.title AS lesson_title,
        u.id AS unit_id,
        u.unit_number,
        u.title AS unit_title,
        l.id AS level_id,
        l.name AS level_name,
        l.slug AS level_slug,
        b.id AS book_id,
        b.name AS book_name,
        b.slug AS book_slug
     FROM activities a
     INNER JOIN lessons le ON le.id = a.lesson_id
     INNER JOIN units u ON u.id = le.unit_id
     INNER JOIN levels l ON l.id = u.level_id
     INNER JOIN books b ON b.id = l.book_id
     WHERE a.id = ?
       AND le.id = ?
       AND u.id = ?
       AND l.id = ?
       AND b.slug = ?
       {$activityStatusWhere}
     LIMIT 1"
);
$contextStmt->execute([$activityId, $lessonId, $unitId, $levelId, $bookSlug]);
$activity = $contextStmt->fetch(PDO::FETCH_ASSOC);

if (!$activity) {
    http_response_code(404);
    exit('Không tìm thấy Speaking Sentences trong cấp độ đã chọn.');
}

$activityPronunciationFocus = trim(
    (string)($activity['pronunciation_focus'] ?? 'none')
);

if ($activityPronunciationFocus === '') {
    $activityPronunciationFocus = 'none';
}

$pronunciationEngine = new PronunciationFeedbackEngine(
    __DIR__ . '/student-runtime/engines'
);

$pronunciationConfig = $pronunciationEngine
    ->buildStudentConfig($activityPronunciationFocus);

$xpReward = max(0, (int)($activity['xp_reward'] ?? 0));
if ($xpReward <= 0) {
    $xpReward = 15;
}

/*
 * Sentence Set được lấy theo thứ tự:
 * 1. activities.sentence_set_id (nếu dự án có cột này)
 * 2. activity_settings.settings_json.resource_id
 */
$sentenceSetId = max(0, (int)($activity['sentence_set_id'] ?? 0));

if ($sentenceSetId <= 0 && slrColumns($pdo, 'activity_settings') !== []) {
    $settingsStmt = $pdo->prepare(
        'SELECT settings_json FROM activity_settings WHERE activity_id = ? LIMIT 1'
    );
    $settingsStmt->execute([$activityId]);
    $settingsJson = $settingsStmt->fetchColumn();

    if (is_string($settingsJson) && $settingsJson !== '') {
        $decodedSettings = json_decode($settingsJson, true);

        if (is_array($decodedSettings)) {
            $resourceType = trim((string)($decodedSettings['resource_type'] ?? ''));
            $resourceId = max(0, (int)($decodedSettings['resource_id'] ?? 0));

            if ($resourceType === 'sentence_set' && $resourceId > 0) {
                $sentenceSetId = $resourceId;
            }
        }
    }
}


/*
 * Runtime settings.
 * These keys are optional. Safe defaults preserve the current activity.
 */
$activitySettings = [];

if (slrColumns($pdo, 'activity_settings') !== []) {
    $runtimeSettingsStmt = $pdo->prepare(
        'SELECT settings_json FROM activity_settings WHERE activity_id = ? LIMIT 1'
    );
    $runtimeSettingsStmt->execute([$activityId]);
    $runtimeSettingsJson = $runtimeSettingsStmt->fetchColumn();

    if (is_string($runtimeSettingsJson) && $runtimeSettingsJson !== '') {
        $decodedRuntimeSettings = json_decode($runtimeSettingsJson, true);

        if (is_array($decodedRuntimeSettings)) {
            $activitySettings = $decodedRuntimeSettings;
        }
    }
}

$runtimeSettings = [
    'show_ipa' => filter_var(
        $activitySettings['show_ipa'] ?? true,
        FILTER_VALIDATE_BOOL
    ),
    'show_image' => filter_var(
        $activitySettings['show_image'] ?? true,
        FILTER_VALIDATE_BOOL
    ),
    'auto_play' => filter_var(
        $activitySettings['auto_play'] ?? false,
        FILTER_VALIDATE_BOOL
    ),
    'show_listen_button' => filter_var(
        $activitySettings['show_listen_button'] ?? true,
        FILTER_VALIDATE_BOOL
    ),
    'allow_retry' => filter_var(
        $activitySettings['allow_retry'] ?? true,
        FILTER_VALIDATE_BOOL
    ),
    'auto_next' => filter_var(
        $activitySettings['auto_next'] ?? false,
        FILTER_VALIDATE_BOOL
    ),
    'passing_score' => max(
        0,
        min(100, (int)($activitySettings['passing_score'] ?? 80))
    ),
    'sentences_per_session' => max(
        1,
        min(
            100,
            (int)(
                $activitySettings['sentences_per_session']
                ?? $activitySettings['sentence_count']
                ?? 10
            )
        )
    ),
    'audio_source' => in_array(
        (string)($activitySettings['audio_source'] ?? 'auto'),
        ['auto', 'speech_synthesis', 'tts_file'],
        true
    )
        ? (string)($activitySettings['audio_source'] ?? 'auto')
        : 'auto',
    'unlimited_practice' => filter_var(
        $activitySettings['unlimited_practice'] ?? true,
        FILTER_VALIDATE_BOOL
    ),
    'attempts' => max(
        1,
        min(10, (int)($activitySettings['attempts'] ?? 3))
    ),
];

/*
 * Sentence images are resolved centrally by:
 * includes/sentence_image_resolver.php
 *
 * Speaking Sentences does not query Word Library for images itself.
 */
$cards = [];

if (
    $sentenceSetId > 0
    && $sentenceColumns !== []
    && $relationColumns !== []
    && $relationTable !== ''
) {
    $setFk = isset($relationColumns['set_id'])
        ? 'set_id'
        : (isset($relationColumns['sentence_set_id'])
            ? 'sentence_set_id'
            : null);

    $sentenceFk = isset($relationColumns['sentence_id'])
        ? 'sentence_id'
        : (isset($relationColumns['item_id']) ? 'item_id' : null);

    $orderColumn = isset($relationColumns['order_no'])
        ? 'order_no'
        : (isset($relationColumns['sort_order'])
            ? 'sort_order'
            : (isset($relationColumns['position']) ? 'position' : 'id'));

    $sentenceTextColumn = isset($sentenceColumns['sentence_text'])
        ? 'sentence_text'
        : (isset($sentenceColumns['sentence'])
            ? 'sentence'
            : (isset($sentenceColumns['text'])
                ? 'text'
                : (isset($sentenceColumns['content']) ? 'content' : null)));

    $ipaColumn = isset($sentenceColumns['phonetic'])
        ? 'phonetic'
        : (isset($sentenceColumns['ipa']) ? 'ipa' : null);

    $imageColumn = isset($sentenceColumns['image_path'])
        ? 'image_path'
        : (isset($sentenceColumns['image']) ? 'image' : null);

    $audioColumn = isset($sentenceColumns['audio_us_path'])
        ? 'audio_us_path'
        : (isset($sentenceColumns['audio_path'])
            ? 'audio_path'
            : (isset($sentenceColumns['audio']) ? 'audio' : null));

    if (
        $setFk !== null
        && $sentenceFk !== null
        && $sentenceTextColumn !== null
    ) {
        $ipaSelect = $ipaColumn !== null
            ? "s.`{$ipaColumn}` AS ipa"
            : "'' AS ipa";

        $imageSelect = $imageColumn !== null
            ? "s.`{$imageColumn}` AS image_path"
            : "'' AS image_path";

        $audioSelect = $audioColumn !== null
            ? "s.`{$audioColumn}` AS audio_path"
            : "'' AS audio_path";

        $sentenceStatusWhere = isset($sentenceColumns['status'])
            ? "AND s.status = 'active'"
            : '';

        $sentenceStmt = $pdo->prepare(
            "SELECT
                s.id,
                s.`{$sentenceTextColumn}` AS sentence_text,
                {$ipaSelect},
                {$imageSelect},
                {$audioSelect},
                rel.`{$orderColumn}` AS display_order
             FROM `{$relationTable}` rel
             INNER JOIN sentences s ON s.id = rel.`{$sentenceFk}`
             WHERE rel.`{$setFk}` = ?
               {$sentenceStatusWhere}
             ORDER BY rel.`{$orderColumn}` ASC, rel.id ASC"
        );
        $sentenceStmt->execute([$sentenceSetId]);
        $sentenceRows = $sentenceStmt->fetchAll(PDO::FETCH_ASSOC);

        foreach ($sentenceRows as $row) {
            $sentenceText = trim(
                (string)($row['sentence_text'] ?? '')
            );

            if ($sentenceText === '') {
                continue;
            }

            $resolvedImage = esResolveSentenceImage(
                $pdo,
                $sentenceText,
                (string)($row['image_path'] ?? '')
            );

            $resolvedAudio = trim(
                (string)($row['audio_path'] ?? '')
            );

            $cards[] = [
                'sentence' => $sentenceText,
                'ipa' => trim((string)($row['ipa'] ?? '')),
                'image' => (string)($resolvedImage['url'] ?? ''),
                // Only full-sentence audio may be used here.
                // When empty, JavaScript reads sentence_text with TTS.
                'audio' => slrAssetUrl($resolvedAudio),
                'media_word' => (string)($resolvedImage['matched_word'] ?? ''),
            ];
        }
    }
}

/*
 * Apply the teacher's session size.
 * Value 100 means All.
 */
$sentenceLimit = (int)($runtimeSettings['sentences_per_session'] ?? 10);

if (
    $sentenceLimit > 0
    && $sentenceLimit < 100
    && count($cards) > $sentenceLimit
) {
    $cards = array_slice($cards, 0, $sentenceLimit);
}

$rawUnitNumber = trim((string)$activity['unit_number']);
$rawLessonNumber = trim((string)$activity['lesson_number']);

$displayUnitNumber = preg_match('/(\d+)/', $rawUnitNumber, $unitMatch)
    ? (int)$unitMatch[1]
    : $rawUnitNumber;

$displayLessonNumber = preg_match('/(\d+)/', $rawLessonNumber, $lessonMatch)
    ? (int)$lessonMatch[1]
    : $rawLessonNumber;

$backUrl = $isPreview
    ? '../teacher/activity-preview/index.php?activity_id=' . (int)$activityId
    : 'student-activities.php?' . http_build_query([
        'book' => (string)$activity['book_slug'],
        'level' => (string)$activity['level_slug'],
        'level_id' => (int)$activity['level_id'],
        'unit_id' => (int)$activity['unit_id'],
        'lesson_id' => (int)$activity['lesson_id'],
        'lesson' => $displayLessonNumber,
    ]);

$_SESSION['last_learning_url'] = (string)($_SERVER['REQUEST_URI'] ?? '');
?>
<!doctype html>
<html lang="vi">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<title>Speaking Sentences | EnglishSpeak</title>
<style>
:root{
  --blue:#198fe3;--blue-dark:#0868c7;--soft:#eaf5ff;--page:#f4f8fc;
  --text:#15314b;--muted:#718399;--line:#dfe8f0;--green:#27ae60;
  --orange:#f39c12;--red:#e55353;--width:520px;--shadow:0 7px 22px rgba(31,71,108,.10)
}
*{box-sizing:border-box}
html,body{margin:0;min-height:100%;background:var(--page);color:var(--text);font-family:"Segoe UI",Tahoma,Arial,sans-serif}
body{display:flex;justify-content:center}
button,a{font:inherit;-webkit-tap-highlight-color:transparent}
.app{width:min(100%,var(--width));min-height:100dvh;padding:0 14px 24px;background:#fff;box-shadow:0 0 28px rgba(30,79,115,.08)}
.topbar{position:sticky;top:0;z-index:40;display:grid;grid-template-columns:46px minmax(0,1fr) 46px;align-items:center;gap:9px;min-height:76px;margin:0 -14px 14px;padding:12px 14px;background:linear-gradient(135deg,#3488ef,#17b8e4);color:#fff;box-shadow:0 5px 15px rgba(31,135,210,.22)}
.round{display:grid;width:44px;height:44px;place-items:center;border:0;border-radius:50%;background:rgba(255,255,255,.15);color:#fff;text-decoration:none;font-size:24px;font-weight:850;cursor:pointer}
.heading{text-align:center;min-width:0}.heading strong{display:block;font-size:22px;line-height:1.15}.heading small{display:block;margin-top:4px;color:rgba(255,255,255,.92);font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.lesson{display:grid;grid-template-columns:52px minmax(0,1fr);align-items:center;gap:11px;min-height:72px;margin-bottom:12px;padding:12px 14px;border-radius:20px;background:linear-gradient(135deg,#edf6ff,#e6f1ff)}
.lesson__icon{width:48px;height:48px;display:grid;place-items:center;border-radius:14px;background:#fff;font-size:29px}.lesson strong{font-size:18px}
.progress-row{display:flex;justify-content:space-between;gap:10px;color:#38536c;font-size:13px;font-weight:800}.track{height:9px;margin:8px 0 13px;overflow:hidden;border-radius:999px;background:#dceaf5}.fill{width:0;height:100%;border-radius:inherit;background:linear-gradient(90deg,#1c7ce4,#18a9e7);transition:width .2s}
.practice-card{min-height:calc(100dvh - 292px);padding:15px;border:1px solid #e2eaf1;border-radius:22px;background:#fff;box-shadow:var(--shadow);display:flex;flex-direction:column;align-items:center}
.image-button{
  width:100%;
  height:min(25dvh,205px);
  min-height:155px;
  display:flex;
  align-items:center;
  justify-content:center;
  padding:6px 10px;
  border:0;
  border-radius:18px;
  background:#fbfdff;
  cursor:pointer
}
.image-button img{
  display:block;
  width:auto;
  height:auto;
  max-width:96%;
  max-height:195px;
  object-fit:contain;
  object-position:center center
}
.fallback{font-size:100px;color:var(--blue)}
.fallback[hidden]{display:none!important}
.image-button img[hidden]{display:none!important}
.word{margin:16px 0 0;color:var(--blue-dark);font-size:clamp(27px,7.5vw,40px);line-height:1.3;text-align:center}.ipa{min-height:23px;margin-top:5px;color:#4f7ca3;font-size:17px;font-style:italic}
.actions{display:grid;grid-template-columns:1fr 1fr;gap:10px;width:100%;margin-top:auto;padding-top:20px}.action{height:52px;padding:0 10px;border:1px solid #cbddeb;border-radius:15px;background:#fff;color:var(--text);font-weight:850;cursor:pointer}.action.primary{border-color:var(--blue);background:linear-gradient(135deg,#237fe1,#159ddd);color:#fff}.action.recording{background:#e55353;border-color:#e55353;color:#fff;animation:pulse 1.2s infinite}
@keyframes pulse{50%{box-shadow:0 0 0 8px rgba(229,83,83,.12)}}
.feedback{display:none;width:100%;margin-top:12px;padding:13px 14px;border-radius:15px;font-size:14px;line-height:1.45;text-align:left}.feedback.show{display:block}.feedback.good{border:1px solid #bce5c8;background:#eefbf2;color:#19783a}.feedback.try{border:1px solid #f0d7a5;background:#fff8e8;color:#8a5c08}.feedback strong{display:block;margin-bottom:5px;font-size:16px}.feedback div{line-height:1.55}
.next-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;width:100%;margin-top:10px;padding-top:0}.next-row button{height:52px;padding:0 10px;border:1px solid #cbddeb;border-radius:15px;background:#fff;font-weight:850;cursor:pointer}.next-row .next{border-color:var(--blue);background:var(--blue);color:#fff}
.finish{display:none;min-height:calc(100dvh - 110px);padding:36px 18px;align-items:center;justify-content:center;flex-direction:column;text-align:center}.finish.show{display:flex}.finish__icon{font-size:76px}.finish h2{margin:14px 0 8px;font-size:40px;color:#123e62}.finish p{margin:0;color:var(--muted);font-size:16px}.summary{width:100%;margin-top:22px;padding:5px 17px;border:1px solid var(--line);border-radius:18px;box-shadow:var(--shadow)}.summary-row{display:flex;justify-content:space-between;align-items:center;min-height:58px;border-bottom:1px solid #e5ecf2}.summary-row:last-child{border:0}.summary-row strong{color:var(--blue-dark);font-size:19px}.finish-actions{display:grid;gap:10px;width:100%;margin-top:18px}.finish-actions button,.finish-actions a{min-height:54px;display:grid;place-items:center;border:1px solid var(--blue);border-radius:15px;background:#fff;color:var(--blue-dark);text-decoration:none;font-weight:850}.finish-actions .primary{background:var(--blue);color:#fff}
.empty{padding:50px 20px;text-align:center}
@media(max-width:390px){.app{padding-left:11px;padding-right:11px}.topbar{margin-left:-11px;margin-right:-11px;padding-left:11px;padding-right:11px}.practice-card{padding:12px}.image-button{height:min(27dvh,220px);min-height:165px}.actions{grid-template-columns:1fr 1fr}.action{font-size:12.5px;padding:0 8px}.next-row button{font-size:13px}}

/* EnglishSpeak XP popup */
.xp-popup {
    position: fixed;
    z-index: 10000;
    inset: 0;
    display: none;
    align-items: center;
    justify-content: center;
    padding: 20px;
    background: rgba(18, 49, 74, .38);
    backdrop-filter: blur(5px);
}
.xp-popup.show { display: flex; }
.xp-popup__card {
    width: min(100%, 350px);
    padding: 24px 20px 20px;
    border-radius: 24px;
    background: #fff;
    box-shadow: 0 18px 50px rgba(18, 49, 74, .24);
    text-align: center;
    animation: xpPopupIn .25s ease-out;
}
.xp-popup__icon {
    width: 76px;
    height: 76px;
    display: grid;
    place-items: center;
    margin: 0 auto 10px;
    border-radius: 50%;
    background: linear-gradient(135deg, #fff6bf, #ffe06e);
    font-size: 42px;
}
.xp-popup__title {
    margin: 0;
    color: #15314b;
    font-size: 27px;
    line-height: 1.2;
}
.xp-popup__text {
    margin: 7px 0 0;
    color: #718399;
    font-size: 14px;
}
.xp-popup__score {
    margin: 17px 0;
    color: #f39c12;
    font-size: 36px;
    font-weight: 900;
    line-height: 1;
}
.xp-popup__button {
    width: 100%;
    min-height: 52px;
    border: 0;
    border-radius: 15px;
    background: linear-gradient(135deg, #237fe1, #159ddd);
    color: #fff;
    font-weight: 850;
    cursor: pointer;
}
@keyframes xpPopupIn {
    from { opacity: 0; transform: translateY(14px) scale(.96); }
    to { opacity: 1; transform: translateY(0) scale(1); }
}
.preview-banner{margin:10px 0;padding:9px 12px;border:1px solid #bfdbfe;border-radius:13px;background:#eff6ff;color:#1d4ed8;text-align:center;font-size:13px;font-weight:800}

</style>
</head>
<body>
<main class="app">
<?php if ($isPreview): ?>
<div class="preview-banner">Preview mode — Không lưu XP hoặc tiến trình học sinh.</div>
<?php endif; ?>
<header class="topbar">
  <a class="round" href="<?= htmlspecialchars($backUrl, ENT_QUOTES, 'UTF-8') ?>" aria-label="Quay lại">←</a>
  <div class="heading">
    <strong>Unit <?= $displayUnitNumber ?></strong>
    <small><?= htmlspecialchars((string)($activity['unit_title'] ?? ''), ENT_QUOTES, 'UTF-8') ?></small>
  </div>
  <span class="round" aria-hidden="true" style="visibility:hidden"></span>
</header>

<?php if ($cards === []): ?>
  <section class="empty"><h2>Chưa có câu luyện nói</h2><p>Hãy liên kết Sentence Set trong Activity Settings.</p></section>
<?php else: ?>
  <section id="practiceScreen">
    <div class="lesson"><div class="lesson__icon">🗣️</div><strong>Speaking Sentences</strong></div>
    <div class="progress-row"><span id="progressLabel">1 / <?= count($cards) ?></span><span id="scoreLabel"></span></div>
    <div class="track"><div class="fill" id="progressFill"></div></div>

    <section class="practice-card">
      <button class="image-button" id="imageButton" type="button" aria-label="Nghe câu" <?= !$runtimeSettings['show_image'] ? 'hidden' : '' ?>>
        <img id="wordImage" alt="" hidden>
        <span class="fallback" id="fallback">💬</span>
      </button>
      <h1 class="word" id="wordText" hidden></h1>
      <div class="ipa" id="ipaText" hidden></div>

      <div class="actions">
        <button class="action" id="listenButton" type="button" <?= !$runtimeSettings['show_listen_button'] ? 'hidden' : '' ?>>🔊 Listen</button>
        <button class="action primary" id="speakButton" type="button" title="Bấm một lần để nói. Hệ thống sẽ tự chấm khi em nói xong.">🎤 Bấm để nói</button>
      </div>

      <div class="feedback" id="feedback" aria-live="polite"></div>

      <div class="next-row" id="nextRow" style="display:none">
        <button id="retryButton" type="button" <?= !$runtimeSettings['allow_retry'] ? 'hidden' : '' ?>>↻ Again</button>
        <button class="next" id="nextButton" type="button">Next →</button>
      </div>
    </section>
  </section>

  <section class="finish" id="finishScreen">
    <div class="finish__icon">🎉</div>
    <h2 id="finishTitle">🎉 Great!</h2>
    <p id="finishSubtitle"><?= count($cards) ?> sentences done.</p>
    <div class="summary">
      <div class="summary-row"><span>Done</span><strong><?= count($cards) ?> / <?= count($cards) ?></strong></div>
      <div class="summary-row"><span>Score</span><strong id="finalScore">0%</strong></div>
      <div class="summary-row"><span>Pass</span><strong id="passedCount">0 / <?= count($cards) ?></strong></div>
      <div class="summary-row"><span>Time</span><strong id="durationText">0:00</strong></div>
    </div>
    <p id="finishMessage" style="margin-top:14px;font-weight:800;color:#38536c"></p>
    <div class="finish-actions">
      <a class="primary" id="continueButton" style="display:none" href="student-runtime/index.php?<?= htmlspecialchars(http_build_query([
          'runtime_action' => 'next',
          'current_activity_id' => $activityId,
          'lesson_id' => $lessonId,
          'preview' => $isPreview ? 1 : 0,
      ], '', '&', PHP_QUERY_RFC3986), ENT_QUOTES, 'UTF-8') ?>">➡ Next</a>
      <button id="replayButton" type="button">↻ Again</button>
      <a href="<?= htmlspecialchars($backUrl, ENT_QUOTES, 'UTF-8') ?>">← Back</a>
    </div>
  </section>
<?php endif; ?>
</main>


<?php if (!$isPreview): ?>
<div class="xp-popup" id="xpPopup" role="dialog" aria-modal="true" aria-labelledby="xpPopupTitle">
    <div class="xp-popup__card">
        <div class="xp-popup__icon" aria-hidden="true">⭐</div>
        <h2 class="xp-popup__title" id="xpPopupTitle">🎉 Great!</h2>
        <p class="xp-popup__text">Completion Reward</p>
        <div class="xp-popup__score" id="xpPopupScore">+<?= $xpReward ?> XP</div>
        <button class="xp-popup__button" id="xpPopupButton" type="button">Continue</button>
    </div>
</div>
<?php endif; ?>

<?php if ($cards !== []): ?>
<script src="https://aka.ms/csspeech/jsbrowserpackageraw"></script>
<script>
'use strict';

/* =========================================================
   EnglishSpeak — Speaking Sentences
   Azure Speech SDK Engine
   ĐOẠN 1: Dữ liệu, trạng thái, giao diện và Azure Token
   ========================================================= */

const isPreview = <?= $isPreview ? 'true' : 'false' ?>;
const runtimeSettings = <?= json_encode(
    $runtimeSettings,
    JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
) ?>;
const pronunciationEngineConfig = <?= json_encode(
    $pronunciationConfig,
    JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
) ?>;

const cards = <?= json_encode(
    $cards,
    JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
) ?>;

const $ = id => document.getElementById(id);

/* ---------- Các thành phần giao diện ---------- */

const practiceScreen = $('practiceScreen');
const finishScreen = $('finishScreen');

const imageButton = $('imageButton');
const wordImage = $('wordImage');
const fallback = $('fallback');

const wordText = $('wordText');
const ipaText = $('ipaText');

const progressLabel = $('progressLabel');
const progressFill = $('progressFill');
const scoreLabel = $('scoreLabel');

const listenButton = $('listenButton');
const speakButton = $('speakButton');
const retryButton = $('retryButton');
const nextButton = $('nextButton');
const nextRow = $('nextRow');
const replayButton = $('replayButton');
const continueButton = $('continueButton');

const feedback = $('feedback');
const finalScore = $('finalScore');
const passedCount = $('passedCount');
const durationText = $('durationText');
const finishMessage = $('finishMessage');
const xpPopupScore = $('xpPopupScore');

/* ---------- Trạng thái hoạt động ---------- */

let currentIndex = 0;
let scores = [];
let attempts = Array(cards.length).fill(0);
let activityStartedAt = Date.now();
let autoNextTimer = null;
let progressSavePromise = null;
let progressSaved = false;
let awardedXp = 0;

let speechRecognizer = null;
let isPreparing = false;
let isListening = false;
let stopRequested = false;
let assessmentHandled = false;
let autoStopTimer = null;

let cachedSpeechToken = '';
let cachedSpeechRegion = '';
let speechTokenExpiresAt = 0;

/* ---------- Popup XP ---------- */

function showXpPopup() {
    if (isPreview) {
        return;
    }

    const popup = $('xpPopup');

    if (popup) {
        popup.classList.add('show');
    }
}

function hideXpPopup() {
    const popup = $('xpPopup');

    if (popup) {
        popup.classList.remove('show');
    }
}

$('xpPopupButton')?.addEventListener('click', hideXpPopup);


/* ---------- Lưu tiến trình ---------- */

async function saveActivityProgress(score, elapsedMs) {
    if (isPreview) {
        return {
            ok: true,
            saved: false,
            preview: true,
            xp_added: 0
        };
    }

    if (progressSaved) {
        return {
            ok: true,
            saved: true,
            xp_added: awardedXp
        };
    }

    if (progressSavePromise) {
        return progressSavePromise;
    }

    const payload = {
        activity_id: <?= (int)$activityId ?>,
        score: Math.max(0, Math.min(100, Math.round(Number(score) || 0))),
        duration: Math.max(0, Math.round(Number(elapsedMs) || 0)),
        completed: Math.max(
            0,
            Math.min(100, Math.round(Number(score) || 0))
        ) >= Number(runtimeSettings.passing_score || 80),
        passing_score: Number(runtimeSettings.passing_score || 80),
        preview: false
    };

    progressSavePromise = fetch(
        '/EnglishSpeak/api/save-activity-progress.php',
        {
            method: 'POST',
            credentials: 'same-origin',
            cache: 'no-store',
            headers: {
                'Content-Type': 'application/json',
                'Accept': 'application/json'
            },
            body: JSON.stringify(payload)
        }
    )
        .then(async response => {
            let result = null;

            try {
                result = await response.json();
            } catch (error) {
                throw new Error('Progress API returned invalid data.');
            }

            if (!response.ok || !result?.ok) {
                throw new Error(
                    result?.message || 'Could not save progress.'
                );
            }

            progressSaved = true;
            awardedXp = Math.max(
                0,
                Number(result.xp_added || 0)
            );

            const xpTitle=document.getElementById('xpPopupTitle');
            const xpText=document.querySelector('.xp-popup__text');
            if (awardedXp>0){
                if(xpPopupScore) xpPopupScore.textContent=`+${awardedXp} XP`;
                if(xpText) xpText.textContent = awardedXp >= 10 ? 'Completion Reward' : 'Practice Reward';
                if(xpTitle) xpTitle.textContent='🎉 Great!';
            } else {
                if (xpPopupScore) {
                    xpPopupScore.textContent = result.passed
                        ? `${Math.max(0, Math.min(100, Math.round(Number(score) || 0)))}%`
                        : '';
                }

                if (xpText) {
                    xpText.textContent = result.passed
                        ? ''
                        : String(
                            result.reward_message
                            || `You need ${runtimeSettings.passing_score}% to earn XP.`
                        );
                }

                if (xpTitle) {
                    xpTitle.textContent = result.passed
                        ? '🎉 Great!'
                        : '😊 Good effort!';
                }
            }

            return result;
        })
        .catch(error => {
            console.warn('Save progress failed:', error);
            return {
                ok: false,
                saved: false,
                xp_added: 0,
                message: error?.message || 'Could not save progress.'
            };
        })
        .finally(() => {
            progressSavePromise = null;
        });

    return progressSavePromise;
}

$('xpPopupButton')?.addEventListener('click', hideXpPopup);

/* ---------- Điểm số ---------- */

function getAverageScore() {
    const validScores = scores.filter(Number.isFinite);

    if (validScores.length === 0) {
        return 0;
    }

    const total = validScores.reduce(
        (sum, score) => sum + score,
        0
    );

    return Math.round(total / validScores.length);
}

function updateAverageScore() {
    if (scoreLabel) {
        scoreLabel.textContent = '';
    }
}

function saveBestScore(score) {
    const safeScore = Math.max(
        0,
        Math.min(100, Math.round(Number(score) || 0))
    );

    scores[currentIndex] = Math.max(
        scores[currentIndex] || 0,
        safeScore
    );

    updateAverageScore();
}


function getPassedCount() {
    return scores.filter(
        score => Number(score || 0) >= Number(runtimeSettings.passing_score || 80)
    ).length;
}

function formatDuration(milliseconds) {
    const totalSeconds = Math.max(0, Math.round(milliseconds / 1000));
    const minutes = Math.floor(totalSeconds / 60);
    const seconds = String(totalSeconds % 60).padStart(2, '0');

    return `${minutes}:${seconds}`;
}

function cancelAutoNext() {
    if (autoNextTimer !== null) {
        window.clearTimeout(autoNextTimer);
        autoNextTimer = null;
    }
}

function scheduleAutoNext(score) {
    cancelAutoNext();

    if (
        !runtimeSettings.auto_next
        || Number(score || 0) < Number(runtimeSettings.passing_score || 80)
    ) {
        return;
    }

    autoNextTimer = window.setTimeout(() => {
        autoNextTimer = null;
        goToNextCard();
    }, 1400);
}

function hasReachedAttemptLimit() {
    if (runtimeSettings.unlimited_practice) {
        return false;
    }

    const used = Number(attempts[currentIndex] || 0);
    const maximum = Math.max(1, Number(runtimeSettings.attempts || 3));

    return used >= maximum;
}

function updateAttemptControls() {
    const reachedMaximum = hasReachedAttemptLimit();

    if (speakButton) {
        speakButton.disabled = reachedMaximum;

        if (reachedMaximum) {
            speakButton.textContent = '✓ Đã đủ lượt';
        }
    }

    if (retryButton) {
        retryButton.disabled = reachedMaximum;
    }

    if (reachedMaximum) {
        revealCurrentSentence();

        if (nextRow) {
            nextRow.hidden = false;
            nextRow.style.display = 'grid';
            nextRow.style.gridTemplateColumns = '1fr';
        }

        if (retryButton) {
            retryButton.hidden = true;
        }

        if (nextButton) {
            nextButton.hidden = false;
            nextButton.disabled = false;
        }
    }
}

/* ---------- Feedback ---------- */

function clearFeedback() {
    if (!feedback) {
        return;
    }

    feedback.className = 'feedback';
    feedback.innerHTML = '';
}

function escapeHtml(value) {
    return String(value ?? '')
        .replaceAll('&', '&amp;')
        .replaceAll('<', '&lt;')
        .replaceAll('>', '&gt;')
        .replaceAll('"', '&quot;')
        .replaceAll("'", '&#039;');
}

function revealCurrentSentence() {
    const card = cards[currentIndex];

    if (!wordText || !card) {
        return;
    }

    wordText.textContent = String(card.sentence || '').trim();
    wordText.hidden = wordText.textContent === '';
}

function setPassedActions() {
    if (nextRow) {
        nextRow.style.display = 'grid';
        nextRow.style.gridTemplateColumns = '1fr';
    }

    if (retryButton) {
        retryButton.hidden = true;
        retryButton.disabled = true;
    }

    if (nextButton) {
        nextButton.hidden = false;
        nextButton.disabled = false;
    }
}

function setResultActions(score) {
    const passingScore = Number(runtimeSettings.passing_score || 80);
    const passed = Number(score || 0) >= passingScore;

    if (!nextRow) return passed;

    nextRow.hidden = false;
    nextRow.style.display = 'grid';
    nextRow.style.gridTemplateColumns = passed ? '1fr 1fr' : '1fr';

    if (retryButton) {
        retryButton.hidden = !runtimeSettings.allow_retry;
        retryButton.disabled = !runtimeSettings.allow_retry;
    }

    if (nextButton) {
        nextButton.hidden = !passed;
        nextButton.disabled = !passed;
    }

    return passed;
}

function showFeedback(type, title, message, allowHtml = false) {
    if (!feedback) {
        return;
    }

    const safeType = type === 'good' ? 'good' : 'try';
    const safeTitle = escapeHtml(title);
    const safeMessage = allowHtml
        ? String(message ?? '')
        : escapeHtml(message);

    feedback.className = `feedback show ${safeType}`;
    

    feedback.innerHTML = `
        <strong>${safeTitle}</strong>
        <div>${safeMessage}</div>
    `;
}

/* ---------- Trạng thái nút Speak ---------- */

function setSpeakButtonReady() {
    if (!speakButton) {
        return;
    }

    speakButton.disabled = false;
    speakButton.classList.remove('recording');
    speakButton.textContent = '🎤 Bấm để nói';
}

function setSpeakButtonPreparing() {
    if (!speakButton) {
        return;
    }

    speakButton.disabled = false;
    speakButton.classList.remove('recording');
    speakButton.textContent = '⏳ Connecting...';
}

function setSpeakButtonListening() {
    if (!speakButton) {
        return;
    }

    speakButton.disabled = false;
    speakButton.classList.add('recording');
    speakButton.textContent = '🔴 Listening...';
}

function setSpeakButtonScoring() {
    if (!speakButton) {
        return;
    }

    speakButton.disabled = true;
    speakButton.classList.remove('recording');
    speakButton.textContent = '⏳ Scoring...';
}

/* ---------- Phát câu mẫu ---------- */

function browserTts(text) {
    if (!('speechSynthesis' in window)) {
        return;
    }

    window.speechSynthesis.cancel();

    const utterance = new SpeechSynthesisUtterance(text);

    utterance.lang = 'en-US';
    utterance.rate = 0.72;
    utterance.pitch = 1;

    window.speechSynthesis.speak(utterance);
}

function playSentenceAudio(text, audioUrl = '') {
    const source = String(runtimeSettings.audio_source || 'auto');

    if (source === 'speech_synthesis') {
        browserTts(text);
        return;
    }

    if (audioUrl) {
        const audio = new Audio(audioUrl);

        audio.play().catch(() => {
            browserTts(text);
        });

        return;
    }

    // Auto and TTS-file modes safely fall back when no saved file exists.
    browserTts(text);
}

/* ---------- Hiển thị câu hiện tại ---------- */

function renderCurrentCard() {
    const card = cards[currentIndex];

    if (!card) {
        return;
    }

    if (wordText) {
        wordText.textContent = '';
        wordText.hidden = true;
    }

    if (ipaText) {
        ipaText.textContent = runtimeSettings.show_ipa
            ? String(card.ipa || '').trim()
            : '';
        ipaText.hidden = ipaText.textContent === '';
    }

    if (card.image) {
        wordImage.src = card.image;
        wordImage.alt = card.sentence || '';
        wordImage.hidden = false;
        wordImage.style.display = 'block';

        fallback.hidden = true;
        fallback.style.display = 'none';
    } else {
        wordImage.hidden = true;
        wordImage.style.display = 'none';
        wordImage.removeAttribute('src');
        wordImage.alt = '';

        fallback.hidden = false;
        fallback.style.display = 'block';
    }

    if (progressLabel) {
        progressLabel.textContent =
            `${currentIndex + 1} / ${cards.length}`;
    }

    if (progressFill) {
        progressFill.style.width =
            `${((currentIndex + 1) / cards.length) * 100}%`;
    }

    updateAverageScore();
    clearFeedback();

    if (nextRow) {
        nextRow.style.display = 'none';
        nextRow.style.gridTemplateColumns = '1fr';
    }

    if (retryButton) {
        retryButton.hidden = !runtimeSettings.allow_retry;
        retryButton.disabled = !runtimeSettings.allow_retry;
    }

    if (nextButton) {
        nextButton.hidden = true;
        nextButton.disabled = true;
    }
    if (nextRow) nextRow.style.display='none';
    cancelAutoNext();
    setSpeakButtonReady();
    updateAttemptControls();

    // Speaking Sentences: mỗi câu mới xuất hiện sẽ tự đọc đúng 1 lần.
    // Nút Listen vẫn dùng để học sinh chủ động nghe lại.
    window.speechSynthesis?.cancel();
    window.setTimeout(() => {
        // Chỉ phát nếu học sinh vẫn đang ở đúng thẻ này.
        if (cards[currentIndex] === card) {
            playSentenceAudio(card.sentence, card.audio);
        }
    }, 350);
}

/* ---------- Lấy Azure Speech Token ---------- */

async function getSpeechToken() {
    const now = Date.now();

    if (
        cachedSpeechToken !== ''
        && cachedSpeechRegion !== ''
        && now < speechTokenExpiresAt
    ) {
        return {
            token: cachedSpeechToken,
            region: cachedSpeechRegion
        };
    }

    const response = await fetch(
        '/EnglishSpeak/api/get-speech-token.php',
        {
            method: 'GET',
            credentials: 'same-origin',
            cache: 'no-store',
            headers: {
                'Accept': 'application/json'
            }
        }
    );

    let result;

    try {
        result = await response.json();
    } catch (error) {
        throw new Error(
            'API Azure Token không trả về dữ liệu hợp lệ.'
        );
    }

    if (
        !response.ok
        || !result.ok
        || !result.token
        || !result.region
    ) {
        throw new Error(
            result?.message
            || 'Không lấy được Azure Speech Token.'
        );
    }

    cachedSpeechToken = String(result.token);
    cachedSpeechRegion = String(result.region);

    const expiresIn = Math.max(
        60,
        Number(result.expires_in || 600)
    );

    speechTokenExpiresAt =
        now + Math.max(30000, (expiresIn - 60) * 1000);

    return {
        token: cachedSpeechToken,
        region: cachedSpeechRegion
    };
}

/* ---------- Kiểm tra Azure SDK ---------- */

function ensureSpeechSdkReady() {
    if (typeof SpeechSDK === 'undefined') {
        throw new Error(
            'Azure Speech SDK chưa tải được. '
            + 'Hãy kiểm tra Internet rồi tải lại trang.'
        );
    }
}

/* ===== HẾT ĐOẠN 1 — KHÔNG ĐÓNG THẺ SCRIPT ===== */
/* =========================================================
   ĐOẠN 2 — Azure Speech SDK: tạo recognizer và đọc kết quả
   ========================================================= */

/* ---------- Hủy recognizer cũ ---------- */

function disposeSpeechRecognizer() {
    if (!speechRecognizer) {
        return;
    }

    try {
        speechRecognizer.recognizing = undefined;
        speechRecognizer.recognized = undefined;
        speechRecognizer.canceled = undefined;
        speechRecognizer.sessionStopped = undefined;
        speechRecognizer.close();
    } catch (error) {
        console.warn('Không thể đóng Speech Recognizer:', error);
    }

    speechRecognizer = null;
}

/* ---------- Đọc JSON kết quả Azure an toàn ---------- */

function parseAzureResultJson(result) {
    try {
        const propertyId =
            SpeechSDK.PropertyId
                .SpeechServiceResponse_JsonResult;

        const rawJson = result?.properties?.getProperty(
            propertyId
        );

        if (!rawJson) {
            return null;
        }

        const parsed = JSON.parse(rawJson);

        if (!parsed || typeof parsed !== 'object') {
            return null;
        }

        return parsed;
    } catch (error) {
        console.warn('Không đọc được JSON Azure:', error);
        return null;
    }
}

/* ---------- Lấy NBest đầu tiên an toàn ---------- */

function getBestResult(azureJson) {
    if (
        !azureJson
        || !Array.isArray(azureJson.NBest)
        || azureJson.NBest.length === 0
    ) {
        return null;
    }

    const best = azureJson.NBest[0];

    return best && typeof best === 'object'
        ? best
        : null;
}

/* ---------- Lấy điểm Pronunciation Assessment ---------- */

function getAssessmentScores(bestResult) {
    const assessment =
        bestResult?.PronunciationAssessment;

    if (!assessment || typeof assessment !== 'object') {
        return {
            pronunciation: 0,
            accuracy: 0,
            fluency: 0,
            completeness: 0
        };
    }

    return {
        pronunciation: Math.round(
            Number(
                assessment.PronScore
                ?? assessment.PronunciationScore
                ?? 0
            )
        ),

        accuracy: Math.round(
            Number(assessment.AccuracyScore ?? 0)
        ),

        fluency: Math.round(
            Number(assessment.FluencyScore ?? 0)
        ),

        completeness: Math.round(
            Number(assessment.CompletenessScore ?? 0)
        )
    };
}

/* ---------- Bộ nhớ phản hồi theo từng câu ---------- */

let previousAttemptByCard = {};

/* ---------- Chuẩn hóa ký hiệu âm Azure ---------- */

function normalizePhoneme(phoneme) {
    const clean = String(phoneme || '')
        .trim()
        .toLowerCase();

    const aliases = {
        'th': 'θ',
        'dh': 'ð',
        'sh': 'ʃ',
        'ch': 'tʃ',
        'jh': 'dʒ',
        'ng': 'ŋ'
    };

    return aliases[clean] || clean;
}

function getStudentSoundLabel(phoneme) {
    const clean = normalizePhoneme(phoneme);

    const labels = {
        'θ': 'th',
        'ð': 'th',
        'ʃ': 'sh',
        'tʃ': 'ch',
        'dʒ': 'j',
        'ŋ': 'ng'
    };

    return labels[clean] || clean;
}

/* ---------- Tìm một phụ âm ưu tiên cần sửa ---------- */

function findPriorityPhoneme(bestResult) {
    const words = Array.isArray(bestResult?.Words)
        ? bestResult.Words
        : [];

    /*
     * Teacher Goal First:
     * - Nếu Activity có Pronunciation Focus, chỉ xét đúng âm/vị trí đó.
     * - Nếu focus = none, dùng danh sách ưu tiên sư phạm mặc định.
     * - Mỗi lần chỉ trả về một lỗi.
     */
    const configuredFocus =
        pronunciationEngineConfig.focus || {};

    const focusKey = String(
        pronunciationEngineConfig.focus_key || 'none'
    );

    const focusType = String(
        configuredFocus.type || 'none'
    );

    const focusSounds = Array.isArray(configuredFocus.sounds)
        ? configuredFocus.sounds.map(normalizePhoneme)
        : [];

    const hasTeacherFocus =
        focusKey !== 'none'
        && focusType !== 'none'
        && focusSounds.length > 0;

    const initialPriority = [
        'θ', 'ð', 'ʃ', 'f', 'v',
        'tʃ', 'dʒ', 'r', 'l', 'w', 'j',
        'k', 't', 'p', 'd', 's', 'z'
    ];

    const finalPriority = [
        'k', 't', 'p', 'd', 'f',
        'θ', 'ð', 'ʃ', 's', 'z',
        'tʃ', 'dʒ', 'ŋ', 'm', 'n', 'l', 'r', 'v'
    ];

    const thresholds =
        pronunciationEngineConfig.score_thresholds || {};

    const clearErrorMax = Number(
        thresholds.clear_error_max ?? 67
    );

    const candidates = [];

    words.forEach(wordItem => {
        const phonemes = Array.isArray(wordItem?.Phonemes)
            ? wordItem.Phonemes
            : [];

        phonemes.forEach((phonemeItem, index) => {
            const score = Number(
                phonemeItem?.PronunciationAssessment?.AccuracyScore ?? 100
            );

            const normalized = normalizePhoneme(
                phonemeItem?.Phoneme
            );

            const isInitial = index === 0;
            const isFinal = index === phonemes.length - 1;

            if (
                score > clearErrorMax
                || (!isInitial && !isFinal)
            ) {
                return;
            }

            const position = isFinal ? 'final' : 'initial';

            if (hasTeacherFocus) {
                const matchesPosition =
                    focusType === position;

                const matchesSound =
                    focusSounds.includes(normalized);

                if (!matchesPosition || !matchesSound) {
                    return;
                }

                candidates.push({
                    phoneme: normalized,
                    score,
                    priorityIndex: 0,
                    position: isFinal ? 'cuối' : 'đầu',
                    word: String(
                        wordItem?.Word || ''
                    ).trim()
                });

                return;
            }

            const priorityList = isFinal
                ? finalPriority
                : initialPriority;

            const priorityIndex =
                priorityList.indexOf(normalized);

            if (priorityIndex === -1) {
                return;
            }

            candidates.push({
                phoneme: normalized,
                score,
                priorityIndex,
                position: isFinal ? 'cuối' : 'đầu',
                word: String(
                    wordItem?.Word || ''
                ).trim()
            });
        });
    });

    if (candidates.length === 0) {
        return null;
    }

    candidates.sort((a, b) => {
        if (a.priorityIndex !== b.priorityIndex) {
            return a.priorityIndex - b.priorityIndex;
        }

        return a.score - b.score;
    });

    return candidates[0];
}

/* ---------- Phản hồi sư phạm ngắn, rõ ---------- */

function buildCorrectionFeedback(issue) {
    if (!issue) {
        return '';
    }

    const word = String(issue.word || '').trim();

    const sound = getStudentSoundLabel(
        issue.phoneme
    );

    if (issue.position === 'cuối') {
        return `Từ ${word}: nhớ đọc rõ /${sound}/ ở cuối.`;
    }

    return `Từ ${word}: nhớ đọc rõ /${sound}/.`;
}

function getSuccessTitle(score) {
    const thresholds =
        pronunciationEngineConfig.score_thresholds || {};

    const messages =
        pronunciationEngineConfig.messages?.success || {};

    const perfectMin = Number(
        thresholds.perfect_min ?? 99
    );

    const excellentMin = Number(
        thresholds.excellent_min ?? 95
    );

    if (score >= perfectMin) {
        return `${messages.perfect || '⭐ Perfect!'} ${score}%`;
    }

    if (score >= excellentMin) {
        return `${messages.excellent || '🌟 Excellent!'} ${score}%`;
    }

    return `${messages.great || '👏 Great!'} ${score}%`;
}

/* ---------- Hiển thị kết quả chấm ---------- */


/* ---------- Kiểm tra học sinh có đọc đúng câu mục tiêu ---------- */

function normalizeSentenceForMatch(value) {
    return String(value || '')
        .toLowerCase()
        .replace(/[’‘]/g, "'")
        .replace(/[^a-z0-9'\s]/g, ' ')
        .replace(/\s+/g, ' ')
        .trim();
}

function isTargetSentenceMatched(recognizedText, targetSentence) {
    const recognized = normalizeSentenceForMatch(recognizedText);
    const target = normalizeSentenceForMatch(targetSentence);

    if (recognized === '' || target === '') {
        return false;
    }

    /*
     * Listen & Repeat Sentence yêu cầu học sinh đọc đúng toàn bộ câu.
     * Chỉ bỏ qua khác biệt chữ hoa, dấu câu và khoảng trắng.
     * Ví dụ:
     * - Target: "This is a fish."
     * - Student: "This is a bike."
     * => Không đạt, dù Pronunciation Score cao.
     */
    return recognized === target;
}

function displayAssessmentResult(
    recognitionResult,
    azureJson
) {
    const bestResult = getBestResult(azureJson);

    const recognizedText = String(
        bestResult?.Display
        ?? recognitionResult?.text
        ?? ''
    ).trim();

    const currentSentence = String(
        cards[currentIndex]?.sentence || ''
    ).trim();

    if (!bestResult || recognizedText === '') {
        revealCurrentSentence();
        cancelAutoNext();
        setResultActions(0);

        showFeedback(
            'try',
            '🎤 Let\'s try again!',
            'Chưa nghe thấy giọng.',
            true
        );

        return;
    }

    /*
     * CONTENT FIRST:
     * Phải đọc đúng câu mục tiêu trước khi xét điểm phát âm.
     * Vì vậy "This is a bike." không thể vượt qua
     * khi câu mục tiêu là "This is a fish.".
     */
    if (!isTargetSentenceMatched(
        recognizedText,
        currentSentence
    )) {
        revealCurrentSentence();
        cancelAutoNext();
        setResultActions(0);

        showFeedback(
            'try',
            '😊 Try again!',
            'Hãy nghe lại và đọc đúng câu bên trên nhé.',
            true
        );

        return;
    }

    const assessmentScores =
        getAssessmentScores(bestResult);

    const pronunciation =
        assessmentScores.pronunciation;

    if (pronunciation <= 0) {
        revealCurrentSentence();
        cancelAutoNext();
        setResultActions(0);

        showFeedback(
            'try',
            '🎤 Let\'s try again!',
            'Chưa nghe thấy giọng.',
            true
        );

        return;
    }

    const passingScore = Number(
        runtimeSettings.passing_score || 80
    );

    const previousScore = Number(
        previousAttemptByCard[currentIndex]?.score || 0
    );

    const thresholds =
        pronunciationEngineConfig.score_thresholds || {};

    const improvementMin = Number(
        thresholds.improvement_min ?? 4
    );

    const improved =
        previousScore > 0
        && pronunciation >= previousScore + improvementMin;

    previousAttemptByCard[currentIndex] = {
        score: Math.max(previousScore, pronunciation)
    };

    saveBestScore(pronunciation);
    updateAttemptControls();

    const passedCurrentSentence =
        setResultActions(pronunciation);

    if (passedCurrentSentence) {
        showFeedback(
            'good',
            '🎉 Great!',
            `${pronunciation}%${improved ? '<br>New Best!' : ''}`,
            true
        );

        const passedCard = cards[currentIndex];

        if (passedCard) {
            window.setTimeout(() => {
                playSentenceAudio(
                    passedCard.sentence,
                    passedCard.audio
                );
            }, 250);
        }

        scheduleAutoNext(pronunciation);
        return;
    }

    cancelAutoNext();
    revealCurrentSentence();

    const priorityIssue =
        findPriorityPhoneme(bestResult);

    const correction =
        buildCorrectionFeedback(priorityIssue);

    if (correction !== '') {
        showFeedback(
            'try',
            `😊 Good effort! ${pronunciation}%`,
            `You need ${passingScore}% to continue.<br><br>${escapeHtml(correction)}<br>Try Again`,
            true
        );

        return;
    }

    if (
        assessmentScores.completeness > 0
        && assessmentScores.completeness < 75
    ) {
        showFeedback(
            'try',
            `😊 Good effort! ${pronunciation}%`,
            `You need ${passingScore}% to continue.<br><br>Đọc đủ cả câu nhé.<br>Try Again`,
            true
        );

        return;
    }

    showFeedback(
        'try',
        `😊 Good effort! ${pronunciation}%`,
        `You need ${passingScore}% to continue.<br><br>Thử đọc chậm và rõ hơn một chút nhé.<br>Try Again`,
        true
    );
}


/* ===== HẾT ĐOẠN 2 — KHÔNG ĐÓNG THẺ SCRIPT ===== */
/* =========================================================
   ĐOẠN 3 — Bấm một lần để nói, Azure tự chấm khi nói xong
   ========================================================= */

/* ---------- Tạo Speech Recognizer ---------- */

async function createSpeechRecognizer() {
    ensureSpeechSdkReady();

    const auth = await getSpeechToken();

    const speechConfig =
        SpeechSDK.SpeechConfig.fromAuthorizationToken(
            auth.token,
            auth.region
        );

    speechConfig.speechRecognitionLanguage = 'en-US';
    speechConfig.outputFormat =
        SpeechSDK.OutputFormat.Detailed;

    const audioConfig =
        SpeechSDK.AudioConfig
            .fromDefaultMicrophoneInput();

    const recognizer =
        new SpeechSDK.SpeechRecognizer(
            speechConfig,
            audioConfig
        );

    const targetSentence = String(
        cards[currentIndex]?.sentence || ''
    ).trim();

    const pronunciationConfig =
        new SpeechSDK.PronunciationAssessmentConfig(
            targetSentence,
            SpeechSDK
                .PronunciationAssessmentGradingSystem
                .HundredMark,
            SpeechSDK
                .PronunciationAssessmentGranularity
                .Phoneme,
            false
        );

    pronunciationConfig.applyTo(recognizer);

    return recognizer;
}

/* ---------- Bộ hẹn giờ tự kết thúc ---------- */

function clearAutoStopTimer() {
    if (autoStopTimer !== null) {
        window.clearTimeout(autoStopTimer);
        autoStopTimer = null;
    }
}

async function finishRecognizedAssessment(result, azureJson) {
    if (assessmentHandled) {
        return;
    }

    assessmentHandled = true;
    clearAutoStopTimer();
    setSpeakButtonScoring();

    // Giữ trạng thái "Scoring" đủ để học sinh nhìn thấy.
    await new Promise(resolve => window.setTimeout(resolve, 220));

    displayAssessmentResult(result, azureJson);

    if (speechRecognizer) {
        try {
            await new Promise((resolve, reject) => {
                speechRecognizer.stopContinuousRecognitionAsync(resolve, reject);
            });
        } catch (error) {
            console.warn('Không tự dừng được Azure Speech:', error);
        }
    }

    isPreparing = false;
    isListening = false;
    disposeSpeechRecognizer();
    setSpeakButtonReady();
    updateAttemptControls();
}

async function finishNoSpeechAssessment() {
    if (assessmentHandled) {
        return;
    }

    assessmentHandled = true;
    clearAutoStopTimer();
    setSpeakButtonScoring();

    if (speechRecognizer) {
        try {
            await new Promise((resolve, reject) => {
                speechRecognizer.stopContinuousRecognitionAsync(resolve, reject);
            });
        } catch (error) {}
    }

    showFeedback(
        'try',
        '🎤 Let\'s try again!',
        'Chưa nhận được giọng nói. Em nói trọn câu, gần micro rồi thử lại nhé.',
        true
    );
    setResultActions(0);

    isPreparing = false;
    isListening = false;
    disposeSpeechRecognizer();
    setSpeakButtonReady();
    updateAttemptControls();
}

/* ---------- Bắt đầu nghe ---------- */

async function startSpeakingAssessment() {
    if (isPreparing || isListening) {
        return;
    }

    if (hasReachedAttemptLimit()) {
        updateAttemptControls();
        return;
    }

    attempts[currentIndex] = Number(attempts[currentIndex] || 0) + 1;
    isPreparing = true;
    stopRequested = false;
    assessmentHandled = false;
    clearAutoStopTimer();

    clearFeedback();
    setSpeakButtonPreparing();

    // Tránh tiếng TTS đang phát lọt vào micro.
    window.speechSynthesis?.cancel();

    try {
        disposeSpeechRecognizer();

        speechRecognizer = await createSpeechRecognizer();

        speechRecognizer.recognizing = (sender, event) => {
            if (
                event?.result?.text
                && String(event.result.text).trim() !== ''
            ) {
                setSpeakButtonListening();
            }
        };

        speechRecognizer.recognized = (sender, event) => {
            const result = event?.result;

            if (!result) {
                return;
            }

            if (
                result.reason
                !== SpeechSDK.ResultReason.RecognizedSpeech
            ) {
                return;
            }

            const azureJson = parseAzureResultJson(result);
            finishRecognizedAssessment(result, azureJson);
        };

        speechRecognizer.canceled = (sender, event) => {
            console.warn('Azure Speech canceled:', event);

            const errorDetails = String(event?.errorDetails || '');

            if (errorDetails.toLowerCase().includes('token')) {
                cachedSpeechToken = '';
                cachedSpeechRegion = '';
                speechTokenExpiresAt = 0;
            }

            if (!assessmentHandled && (isListening || isPreparing)) {
                clearAutoStopTimer();
                isListening = false;
                isPreparing = false;
                disposeSpeechRecognizer();
                setSpeakButtonReady();
                showFeedback(
                    'try',
                    'Thử lại nhé',
                    'Azure chưa nghe rõ. Em kiểm tra micro rồi thử lại nhé.'
                );
                updateAttemptControls();
            }
        };

        speechRecognizer.sessionStopped = () => {
            isListening = false;
            isPreparing = false;
            if (!assessmentHandled) {
                clearAutoStopTimer();
                setSpeakButtonReady();
            }
        };

        await new Promise((resolve, reject) => {
            speechRecognizer.startContinuousRecognitionAsync(resolve, reject);
        });

        isPreparing = false;
        isListening = true;
        setSpeakButtonListening();

        // Câu dài hơn từ đơn, cho tối đa 12 giây để đọc trọn câu.
        // Nếu Azure không trả kết quả thì báo chưa nhận giọng, không tự gán điểm thấp.
        autoStopTimer = window.setTimeout(() => {
            if (isListening && !assessmentHandled) {
                finishNoSpeechAssessment();
            }
        }, 12000);
    } catch (error) {
        isPreparing = false;
        isListening = false;
        clearAutoStopTimer();

        disposeSpeechRecognizer();
        setSpeakButtonReady();
        updateAttemptControls();

        showFeedback(
            'try',
            'Chưa kết nối được Azure Speech',
            error?.message
            || 'Em kiểm tra Internet và micro rồi thử lại nhé.'
        );
    }
}

/* ---------- Dừng chủ động chỉ dùng khi rời trang/chuyển trạng thái ---------- */

async function stopSpeakingAssessment() {
    stopRequested = true;
    clearAutoStopTimer();

    if (isPreparing) {
        return;
    }

    if (!isListening || !speechRecognizer) {
        return;
    }

    isListening = false;
    setSpeakButtonScoring();

    try {
        await new Promise((resolve, reject) => {
            speechRecognizer.stopContinuousRecognitionAsync(resolve, reject);
        });
    } catch (error) {
        console.warn('Không dừng được Azure Speech:', error);
    } finally {
        isPreparing = false;
        isListening = false;
        disposeSpeechRecognizer();
        setSpeakButtonReady();
    }
}

/* ---------- Chuyển sang câu tiếp theo ---------- */

const finishMessages = [
    {title:'Great!', subtitle:'Practice complete.', message:'Well done!'},
    {title:'Excellent!', subtitle:'You did it!', message:'Good job!'},
    {title:'Awesome!', subtitle:'All done!', message:'Nice work!'}
];

async function goToNextCard() {
    if (isPreparing || isListening) {
        return;
    }

    const passingScore = Number(runtimeSettings.passing_score || 80);
    const currentScore = Number(scores[currentIndex] || 0);

    if (currentScore < passingScore && !hasReachedAttemptLimit()) {
        revealCurrentSentence?.();
        showFeedback(
            'try',
            '😊 Good effort!',
            `You need ${passingScore}% to continue.<br>Try Again`,
            true
        );
        setResultActions(currentScore);
        return;
    }

    if (currentIndex < cards.length - 1) {
        currentIndex++;
        renderCurrentCard();

        return;
    }

    practiceScreen.style.display = 'none';
    finishScreen.classList.add('show');

    const average = getAverageScore();
    const passed = getPassedCount();
    const elapsed = Date.now() - activityStartedAt;

    if (finalScore) {
        finalScore.textContent = `${average}/100`;
    }

    if (passedCount) {
        passedCount.textContent = average >= Number(runtimeSettings.passing_score||80) ? 'Yes' : 'No';
    }

    if (durationText) {
        durationText.textContent = formatDuration(elapsed);
    }

    const set = finishMessages[Math.floor(Math.random()*finishMessages.length)];
    document.getElementById('finishTitle').textContent = set.title;
    document.getElementById('finishSubtitle').textContent = set.subtitle;
    if (finishMessage) {
        finishMessage.textContent = '';
        finishMessage.style.display = 'none';
    }

    if (continueButton) {
        continueButton.style.display = 'none';
    }

    const saveResult = await saveActivityProgress(
        average,
        elapsed
    );

    if (continueButton) {
        const passedActivity =
            average >= Number(runtimeSettings.passing_score || 80);

        continueButton.style.display =
            passedActivity && (isPreview || saveResult?.ok)
                ? 'grid'
                : 'none';
    }

    if (
        !isPreview
        && saveResult?.ok
        && (
            Number(saveResult.xp_added || 0) > 0
            || saveResult.passed === true
        )
    ) {
        showXpPopup();
    }
}

/* ---------- Luyện lại từ đầu ---------- */

function restartActivity() {
    stopRequested = true;

    if (isListening || isPreparing) {
        stopSpeakingAssessment();
    }

    currentIndex = 0;
    scores = [];
    attempts = Array(cards.length).fill(0);
    activityStartedAt = Date.now();
    previousAttemptByCard = {};
    progressSaved = false;
    progressSavePromise = null;
    awardedXp = 0;

    if (xpPopupScore) xpPopupScore.textContent='';

    cancelAutoNext();

    finishScreen.classList.remove('show');
    practiceScreen.style.display = 'block';

    hideXpPopup();
    renderCurrentCard();
}

/* ===== HẾT ĐOẠN 3 — KHÔNG ĐÓNG THẺ SCRIPT ===== */
/* =========================================================
   ĐOẠN 4 — Sự kiện giao diện và khởi động hoạt động
   ========================================================= */

/* ---------- Listen ---------- */

listenButton?.addEventListener('click', () => {
    const card = cards[currentIndex];

    if (!card) {
        return;
    }

    playSentenceAudio(
        card.sentence,
        card.audio
    );
});

imageButton?.addEventListener('click', () => {
    const card = cards[currentIndex];

    if (!card) {
        return;
    }

    playSentenceAudio(
        card.sentence,
        card.audio
    );
});

/* ---------- Thử lại ---------- */

retryButton?.addEventListener('click', () => {
    if (isPreparing || isListening) {
        return;
    }

    clearFeedback();

    const card = cards[currentIndex];

    if (card) {
        playSentenceAudio(
            card.sentence,
            card.audio
        );
    }
});

/* ---------- Sang thẻ tiếp theo ---------- */

nextButton?.addEventListener(
    'click',
    goToNextCard
);

/* ---------- Học lại toàn bộ ---------- */

replayButton?.addEventListener(
    'click',
    restartActivity
);

/* ---------- Nói: PC và mobile đều bấm một lần ---------- */

// Không dùng pointerup để dừng nữa. Azure nhận câu rồi tự chấm.
speakButton?.addEventListener('click', event => {
    event.preventDefault();

    if (isPreparing || isListening) {
        return;
    }

    startSpeakingAssessment();
});

/* ---------- Không cho mở menu chuột phải trên nút Speak ---------- */

speakButton?.addEventListener(
    'contextmenu',
    event => {
        event.preventDefault();
    }
);

/* ---------- Dọn dẹp khi rời trang ---------- */

window.addEventListener(
    'beforeunload',
    () => {
        try {
            if (
                speechRecognizer
                && isListening
            ) {
                speechRecognizer
                    .stopContinuousRecognitionAsync(
                        () => {
                            disposeSpeechRecognizer();
                        },
                        () => {
                            disposeSpeechRecognizer();
                        }
                    );

                return;
            }
        } catch (error) {}

        cancelAutoNext();
        disposeSpeechRecognizer();
    }
);

/* ---------- Khởi động ---------- */

renderCurrentCard();

/* =========================================================
   HẾT ENGINE SPEAKING SENTENCES
   ========================================================= */
</script>
<?php endif; ?>
</body>
</html>
