<?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/student_activity_navigation.php';

$pdo = getDbConnection();

$bookSlug = trim((string)($_GET['book'] ?? ''));
$levelId = max(0, (int)($_GET['level_id'] ?? 0));
$unitId = max(0, (int)($_GET['unit_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 dlgAssetUrl(string $path): string
{
    $path = trim($path);
    if ($path === '') return '';
    if (preg_match('#^(https?://|data:)#i', $path)) return $path;

    $relative = preg_replace('#^/EnglishSpeak/#i', '', $path);
    $relative = ltrim((string)$relative, './');
    $url = '/EnglishSpeak/' . $relative;

    // Cache-bust local assets when the underlying file changes.
    $file = dirname(__DIR__) . '/' . $relative;
    if (is_file($file)) {
        $url .= '?v=' . (string)filemtime($file);
    }

    return $url;
}

function dlgTableExists(PDO $pdo, string $table): bool
{
    $stmt = $pdo->prepare(
        "SELECT COUNT(*) FROM information_schema.TABLES
         WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?"
    );
    $stmt->execute([$table]);
    return (int)$stmt->fetchColumn() > 0;
}

function dlgColumnExists(PDO $pdo, string $table, string $column): bool
{
    $stmt = $pdo->prepare(
        "SELECT COUNT(*) FROM information_schema.COLUMNS
         WHERE TABLE_SCHEMA = DATABASE()
           AND TABLE_NAME = ?
           AND COLUMN_NAME = ?"
    );
    $stmt->execute([$table, $column]);
    return (int)$stmt->fetchColumn() > 0;
}

$contextStmt = $pdo->prepare(
    "SELECT
        a.id AS activity_id,
        a.title AS activity_title,
        COALESCE(a.xp_reward, 10) AS xp_reward,
        COALESCE(a.passing_score, 80) AS activity_passing_score,
        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 = ?
       AND a.status = 'active'
     LIMIT 1"
);
$contextStmt->execute([$activityId, $lessonId, $unitId, $levelId, $bookSlug]);
$activity = $contextStmt->fetch(PDO::FETCH_ASSOC);

if (!$activity) {
    http_response_code(404);
    exit('Dialogue activity not found.');
}

$settings = [
    'resource_type' => 'dialogue_set',
    'resource_id' => 0,
    'dialogue_turns' => 6,
    'passing_score' => (int)($activity['activity_passing_score'] ?? 80),
    'shuffle_answers' => true,
    'auto_play_prompt' => true,
    'speak_correct_response' => true,
    'show_progress' => true,
];

if (dlgTableExists($pdo, 'activity_settings')) {
    $stmt = $pdo->prepare('SELECT settings_json FROM activity_settings WHERE activity_id = ? LIMIT 1');
    $stmt->execute([$activityId]);
    $json = $stmt->fetchColumn();
    if (is_string($json) && $json !== '') {
        $decoded = json_decode($json, true);
        if (is_array($decoded)) {
            $settings = array_merge($settings, $decoded);
        }
    }
}

$dialogueSetId = max(0, (int)($settings['resource_id'] ?? 0));
$turns = [];

if ($dialogueSetId > 0 && dlgTableExists($pdo, 'dialogue_sets') && dlgTableExists($pdo, 'dialogue_items')) {
    $limit = max(1, min(50, (int)($settings['dialogue_turns'] ?? 6)));

    $acceptedResponsesSql = dlgColumnExists($pdo, 'dialogue_items', 'accepted_responses')
        ? ', accepted_responses'
        : '';

    $dialogueImageSql = (
        dlgColumnExists($pdo, 'dialogue_items', 'image_word_id')
        && dlgTableExists($pdo, 'words')
    )
        ? "COALESCE(NULLIF((SELECT w.image_path FROM words w WHERE w.id = dialogue_items.image_word_id LIMIT 1), ''), image_path) AS image_path"
        : "image_path";

    $stmt = $pdo->prepare(
        "SELECT id, speaker_name, prompt_text, correct_response
                {$acceptedResponsesSql},
                distractor_1, distractor_2, distractor_3,
                prompt_audio, response_audio, {$dialogueImageSql}
         FROM dialogue_items
         WHERE dialogue_set_id = ?
           AND status = 'active'
         ORDER BY sort_order, id
         LIMIT {$limit}"
    );
    $stmt->execute([$dialogueSetId]);

    foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
        $primaryCorrect = trim((string)$row['correct_response']);
        $correctResponses = [$primaryCorrect];

        if (isset($row['accepted_responses']) && trim((string)$row['accepted_responses']) !== '') {
            $decodedAccepted = json_decode((string)$row['accepted_responses'], true);
            if (is_array($decodedAccepted)) {
                foreach ($decodedAccepted as $accepted) {
                    $accepted = trim((string)$accepted);
                    if ($accepted !== '') $correctResponses[] = $accepted;
                }
            }
        }

        $correctResponses = array_values(array_unique(array_filter(
            $correctResponses,
            static fn(string $v): bool => $v !== ''
        )));

        if (trim((string)$row['prompt_text']) === '' || $correctResponses === []) {
            continue;
        }

        $turns[] = [
            'speaker' => trim((string)$row['speaker_name']) !== '' ? trim((string)$row['speaker_name']) : 'A',
            'prompt' => trim((string)$row['prompt_text']),
            'correct' => $correctResponses[0],
            'correct_responses' => $correctResponses,
            'prompt_audio' => dlgAssetUrl((string)($row['prompt_audio'] ?? '')),
            'response_audio' => dlgAssetUrl((string)($row['response_audio'] ?? '')),
            'image' => dlgAssetUrl((string)($row['image_path'] ?? '')),
        ];
    }
}

$rawUnitNumber = trim((string)$activity['unit_number']);
$displayUnitNumber = preg_match('/(\d+)/', $rawUnitNumber, $m) ? (int)$m[1] : $rawUnitNumber;
$backUrl = $isPreview
    ? '../teacher/activity-preview/index.php?activity_id=' . $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' => (string)$activity['lesson_number'],
    ]);

$nextActivityUrl = englishSpeakGetNextActivityUrl($pdo, $activityId);
if ($isPreview && str_contains($nextActivityUrl, 'student-runtime/index.php')) {
    $nextActivityUrl .= (str_contains($nextActivityUrl, '?') ? '&' : '?') . 'preview=1';
}
?>
<!doctype html>
<html lang="vi">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<title>Dialogue | EnglishSpeak</title>
<style>
:root{--blue:#198fe3;--blue-dark:#0968c7;--page:#f4f8fc;--text:#17334b;--muted:#718399;--line:#dfe8f0;--green:#22a45a;--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 13px 24px;background:#fff;box-shadow:0 0 28px rgba(30,79,115,.08)}
.topbar{position:sticky;top:0;z-index:30;display:grid;grid-template-columns:46px minmax(0,1fr) 46px;align-items:center;gap:9px;min-height:76px;margin:0 -13px 14px;padding:12px 13px;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}
.heading{text-align:center}.heading strong{display:block;font-size:21px}.heading small{display:block;margin-top:4px;font-size:12px;color:rgba(255,255,255,.92)}
.preview-banner{margin:9px 0;padding:9px 12px;border:1px solid #bfdbfe;border-radius:12px;background:#eff6ff;color:#1d4ed8;text-align:center;font-size:12px;font-weight:800}
.progress-row{display:flex;justify-content:space-between;gap:10px;margin-top:5px;color:#38536c;font-size:13px;font-weight:800}.track{height:9px;margin:8px 0 14px;overflow:hidden;border-radius:999px;background:#dceaf5}.fill{height:100%;width:0;border-radius:inherit;background:linear-gradient(90deg,#1c7ce4,#18a9e7);transition:width .2s}
.card{min-height:calc(100dvh - 190px);padding:15px;border:1px solid #e2eaf1;border-radius:22px;background:#fff;box-shadow:var(--shadow);display:flex;flex-direction:column}
.scene{display:flex;justify-content:center;margin-bottom:12px}.scene img{width:150px;height:150px;object-fit:contain;border-radius:20px;background:#f8fafc}.avatar{width:72px;height:72px;display:grid;place-items:center;border-radius:50%;background:#e0f2fe;font-size:34px}
.conversation{display:grid;gap:10px}.bubble{max-width:88%;padding:12px 14px;border-radius:18px;line-height:1.45;font-size:17px;font-weight:800}.bubble.prompt{justify-self:start;border-bottom-left-radius:6px;background:#eff6ff;color:#164e8a}.bubble.response{justify-self:end;border-bottom-right-radius:6px;background:#ecfdf3;color:#166534}
.speaker{font-size:11px;font-weight:900;opacity:.7;margin-bottom:4px;text-transform:uppercase;letter-spacing:.05em}.listen{margin-top:8px;border:0;background:transparent;color:#2563eb;font-weight:850;cursor:pointer}
.speak-area{display:grid;justify-items:center;gap:10px;margin-top:20px}.mic{width:86px;height:86px;border:0;border-radius:50%;background:linear-gradient(135deg,#2586e8,#16a8df);color:#fff;font-size:34px;box-shadow:0 8px 20px rgba(31,135,210,.24);cursor:pointer}.mic.listening{background:#ef4444;animation:pulse 1s infinite}.mic:disabled{opacity:.55;cursor:not-allowed}.mic-label{font-size:13px;font-weight:850;color:#526b80}.heard{min-height:24px;text-align:center;color:#334155;font-size:14px;font-weight:750}.target-hint{display:none;margin-top:6px;padding:9px 12px;border-radius:12px;background:#fff7ed;color:#9a3412;font-size:13px;font-weight:800;text-align:center}.target-hint.show{display:block}
.feedback{min-height:25px;margin-top:12px;text-align:center;color:#64748b;font-size:13px;font-weight:800}.next-row{display:none;margin-top:auto;padding-top:16px}.next-row.show{display:grid}.next{height:52px;border:0;border-radius:15px;background:var(--blue);color:#fff;font-weight:900;cursor:pointer}
@keyframes pulse{50%{transform:scale(1.06)}}
.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:70px}.finish h2{margin:12px 0 6px;font-size:38px;color:#123e62}.summary{width:100%;margin-top:20px;padding:5px 16px;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:56px;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 a,.finish-actions button{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:900;cursor:pointer}.finish-actions .primary{background:var(--blue);color:#fff}
.empty{padding:50px 20px;text-align:center}
.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}.xp-popup__icon{font-size:45px}.xp-popup__title{margin:8px 0 0;font-size:27px}.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}.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}
</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') ?>">←</a>
  <div class="heading"><strong>Unit <?= htmlspecialchars((string)$displayUnitNumber) ?></strong><small>Dialogue • Ask & Answer</small></div>
  <span class="round" style="visibility:hidden"></span>
</header>

<?php if ($turns === []): ?>
<section class="empty"><h2>Chưa có Dialogue</h2><p>Hãy tạo Dialogue Set và chọn nó trong Activity Settings.</p></section>
<?php else: ?>
<section id="gameScreen">
  <div class="progress-row"><span id="progressLabel"></span><span id="scoreLabel"></span></div>
  <div class="track"><div class="fill" id="progressFill"></div></div>
  <section class="card">
    <div class="scene" id="scene"><div class="avatar" id="avatar">💬</div><img id="sceneImage" alt="" hidden></div>
    <div class="conversation">
      <div class="bubble prompt">
        <div class="speaker" id="speakerName"></div>
        <div id="promptText"></div>
        <button class="listen" id="listenPrompt" type="button">🔊 Listen</button>
      </div>
      <div class="bubble response" id="responseBubble" hidden>
        <div class="speaker">You</div>
        <div id="responseText"></div>
      </div>
    </div>
    <div class="speak-area">
      <button class="mic" id="micButton" type="button" aria-label="Bấm để trả lời">🎤</button>
      <div class="mic-label" id="micLabel">Bấm micro rồi trả lời</div>
      <div class="heard" id="heardText"></div>
      <div class="target-hint" id="targetHint"></div>
    </div>
    <div class="feedback" id="feedback"></div>
    <div class="next-row" id="nextRow"><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">Dialogue complete.</p>
  <div class="summary">
    <div class="summary-row"><span>Done</span><strong><?= count($turns) ?>/<?= count($turns) ?></strong></div>
    <div class="summary-row"><span>Score</span><strong id="finalScore">0/100</strong></div>
    <div class="summary-row"><span>Pass</span><strong id="finalPass">No</strong></div>
  </div>
  <div class="finish-actions">
    <a class="primary" id="continueButton" href="<?= htmlspecialchars($nextActivityUrl, ENT_QUOTES, 'UTF-8') ?>">Next</a>
    <button id="againButton" type="button">Again</button>
    <a href="<?= htmlspecialchars($backUrl, ENT_QUOTES, 'UTF-8') ?>">Back</a>
  </div>
</section>
<?php endif; ?>
</main>

<div class="xp-popup" id="xpPopup">
  <div class="xp-popup__card">
    <div class="xp-popup__icon">⭐</div>
    <h2 class="xp-popup__title" id="xpPopupTitle">Great!</h2>
    <p class="xp-popup__text" id="xpPopupText"></p>
    <div class="xp-popup__score" id="xpPopupScore"></div>
    <button class="xp-popup__button" id="xpPopupButton" type="button">Show Result</button>
  </div>
</div>

<?php if ($turns !== []): ?>
<script src="https://aka.ms/csspeech/jsbrowserpackageraw"></script>
<script>
'use strict';
const isPreview = <?= $isPreview ? 'true' : 'false' ?>;
const activityId = <?= $activityId ?>;
const settings = <?= json_encode($settings, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
const turns = <?= json_encode($turns, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;

const $ = id => document.getElementById(id);
const gameScreen=$('gameScreen'), finishScreen=$('finishScreen'),
      promptText=$('promptText'), speakerName=$('speakerName'), responseBubble=$('responseBubble'),
      responseText=$('responseText'), feedback=$('feedback'), nextRow=$('nextRow'),
      progressLabel=$('progressLabel'), progressFill=$('progressFill'), scoreLabel=$('scoreLabel'),
      sceneImage=$('sceneImage'), avatar=$('avatar'), continueButton=$('continueButton'),
      micButton=$('micButton'), micLabel=$('micLabel'), heardText=$('heardText'), targetHint=$('targetHint');

let current=0, turnScores=[], answered=false, hadWrong=false, saving=false, startedAt=Date.now(), recognition=null, listening=false;
let cachedSpeechToken='', cachedSpeechRegion='', speechTokenExpiresAt=0;

function shuffle(a){const c=[...a];for(let i=c.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[c[i],c[j]]=[c[j],c[i]];}return c;}
function speak(text){if(!('speechSynthesis' in window)||!text)return;window.speechSynthesis.cancel();const u=new SpeechSynthesisUtterance(text);u.lang='en-US';u.rate=.78;const voices=window.speechSynthesis.getVoices();u.voice=voices.find(v=>String(v.lang).toLowerCase().startsWith('en-us'))||voices.find(v=>String(v.lang).toLowerCase().startsWith('en'))||null;window.speechSynthesis.speak(u);}
function play(url,text){if(url){const a=new Audio(url);a.play().catch(()=>speak(text));}else speak(text);}

/* Short, gentle game sounds generated in the browser.
   No external audio files are required. */
let dlgAudioCtx=null;
function getDlgAudioCtx(){
  const Ctx=window.AudioContext||window.webkitAudioContext;
  if(!Ctx)return null;
  if(!dlgAudioCtx)dlgAudioCtx=new Ctx();
  if(dlgAudioCtx.state==='suspended')dlgAudioCtx.resume().catch(()=>{});
  return dlgAudioCtx;
}
function dlgTone(freq,start,duration,volume=.055,type='sine'){
  const ctx=getDlgAudioCtx();if(!ctx)return;
  const osc=ctx.createOscillator(),gain=ctx.createGain();
  osc.type=type;osc.frequency.setValueAtTime(freq,ctx.currentTime+start);
  gain.gain.setValueAtTime(.0001,ctx.currentTime+start);
  gain.gain.exponentialRampToValueAtTime(volume,ctx.currentTime+start+.012);
  gain.gain.exponentialRampToValueAtTime(.0001,ctx.currentTime+start+duration);
  osc.connect(gain);gain.connect(ctx.destination);
  osc.start(ctx.currentTime+start);osc.stop(ctx.currentTime+start+duration+.02);
}
function playCorrectSound(){
  // Bright two-note chime.
  dlgTone(660,0,.11,.05,'sine');
  dlgTone(880,.10,.16,.06,'sine');
}
function playWrongSound(){
  // Softer low two-note "try again" cue; intentionally not a harsh buzzer.
  dlgTone(330,0,.10,.035,'sine');
  dlgTone(260,.09,.14,.03,'sine');
}

function score(){return turnScores.length?Math.round(turnScores.reduce((a,b)=>a+b,0)/turns.length):0;}

function normalizeSpeech(s){
  return String(s||'').toLowerCase()
    .replace(/[’']/g,"'")
    .replace(/\b(i'm)\b/g,'i am')
    .replace(/\b(you're)\b/g,'you are')
    .replace(/\b(he's)\b/g,'he is')
    .replace(/\b(she's)\b/g,'she is')
    .replace(/[^a-z0-9\s]/g,' ')
    .replace(/\s+/g,' ').trim();
}
function similarity(a,b){
  a=normalizeSpeech(a);b=normalizeSpeech(b);
  if(!a||!b)return 0;
  if(a===b)return 100;
  const A=a.split(' '),B=b.split(' ');
  const dp=Array.from({length:A.length+1},()=>Array(B.length+1).fill(0));
  for(let i=1;i<=A.length;i++)for(let j=1;j<=B.length;j++){
    dp[i][j]=A[i-1]===B[j-1]?dp[i-1][j-1]+1:Math.max(dp[i-1][j],dp[i][j-1]);
  }
  const lcs=dp[A.length][B.length];
  const precision=lcs/Math.max(1,A.length), recall=lcs/Math.max(1,B.length);
  return Math.round((precision&&recall ? (2*precision*recall)/(precision+recall) : 0)*100);
}
function render(){
  answered=false;hadWrong=false;feedback.textContent='';nextRow.classList.remove('show');responseBubble.hidden=true;
  heardText.textContent='';targetHint.textContent='';targetHint.classList.remove('show');
  micButton.disabled=false;micButton.classList.remove('listening');micLabel.textContent='Bấm micro rồi trả lời';
  const turn=turns[current];speakerName.textContent=turn.speaker||'A';promptText.textContent=turn.prompt;
  progressLabel.textContent=`${current+1} / ${turns.length}`;progressFill.style.width=`${((current+1)/turns.length)*100}%`;scoreLabel.textContent='';
  $('nextButton').textContent=current===turns.length-1?'Finish':'Next';
  if(turn.image){sceneImage.src=turn.image;sceneImage.hidden=false;avatar.hidden=true;}else{sceneImage.hidden=true;sceneImage.removeAttribute('src');avatar.hidden=false;}
  if(settings.auto_play_prompt){setTimeout(()=>play(turn.prompt_audio,turn.prompt),280);}
}
function handleSpeech(transcript){
  if(answered)return;
  const turn=turns[current];
  const correctResponses=Array.isArray(turn.correct_responses)&&turn.correct_responses.length
    ? turn.correct_responses
    : [turn.correct];

  let bestScore=0;
  let bestResponse=turn.correct;
  correctResponses.forEach(response=>{
    let candidateScore=similarity(transcript,response);

    // Không cho câu trả lời bị thiếu từ vẫn vượt Passing Score.
    // Ví dụ: target "My name is Danny" nhưng chỉ nói "my name is"
    // có thể đạt ~86% theo LCS/F1 cũ, dù câu trả lời chưa hoàn chỉnh.
    const spokenNormalized=normalizeSpeech(transcript);
    const targetNormalized=normalizeSpeech(response);
    const spokenWordCount=spokenNormalized ? spokenNormalized.split(' ').length : 0;
    const targetWordCount=targetNormalized ? targetNormalized.split(' ').length : 0;
    const passingScore=Number(settings.passing_score||80);
    if(targetWordCount>0 && spokenWordCount<targetWordCount){
      candidateScore=Math.min(candidateScore,Math.max(0,passingScore-1));
    }

    if(candidateScore>bestScore){
      bestScore=candidateScore;
      bestResponse=response;
    }
  });

  const s=bestScore;
  heardText.textContent=transcript ? `You: ${transcript}` : '';
  scoreLabel.textContent=`${s}%`;

  if(s>=Number(settings.passing_score||80)){
    answered=true;playCorrectSound();turnScores[current]=Math.max(Number(turnScores[current]||0),s);
    responseText.textContent=transcript||bestResponse;responseBubble.hidden=false;
    targetHint.textContent='';targetHint.classList.remove('show');
    feedback.textContent='Great!';micLabel.textContent='';micButton.disabled=true;nextRow.classList.add('show');
    if(settings.speak_correct_response)setTimeout(()=>play(turn.response_audio,bestResponse),220);
  }else{
    hadWrong=true;playWrongSound();turnScores[current]=Math.max(Number(turnScores[current]||0),s);
    feedback.textContent=`Good effort! ${s}% • Try Again`;
    targetHint.textContent=`Gợi ý: ${bestResponse}`;
    targetHint.classList.add('show');
  }
}
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'}
  });
  const result=await response.json().catch(()=>null);
  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};
}

function closeRecognition(){
  if(recognition){try{recognition.close();}catch(e){} recognition=null;}
}

async function startListening(){
  if(answered||listening)return;
  if(typeof SpeechSDK==='undefined'){
    feedback.textContent='Azure Speech SDK chưa tải được. Hãy kiểm tra Internet rồi tải lại trang.';
    return;
  }
  listening=true;micButton.disabled=true;micButton.classList.remove('listening');
  micLabel.textContent='⏳ Đang kết nối...';heardText.textContent='';feedback.textContent='';
  window.speechSynthesis?.cancel();
  try{
    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();
    recognition=new SpeechSDK.SpeechRecognizer(speechConfig,audioConfig);
    micButton.classList.add('listening');micLabel.textContent='🔴 Đang nghe...';
    recognition.recognizeOnceAsync(result=>{
      micButton.classList.remove('listening');micLabel.textContent='⏳ Đang chấm...';
      const ok=result && result.reason===SpeechSDK.ResultReason.RecognizedSpeech;
      const transcript=ok?String(result.text||'').trim().replace(/[.?!]+$/,'').trim():'';
      closeRecognition();listening=false;micButton.disabled=false;
      if(transcript){handleSpeech(transcript);}
      else{feedback.textContent='Chưa nhận được giọng nói. Hãy thử lại.';micLabel.textContent='Bấm micro rồi trả lời';}
    },error=>{
      console.warn('Azure Speech error:',error);closeRecognition();listening=false;
      micButton.classList.remove('listening');micButton.disabled=false;
      micLabel.textContent='Bấm micro rồi trả lời';feedback.textContent='Không nghe rõ. Hãy thử lại.';
    });
  }catch(error){
    console.warn('Azure Speech start failed:',error);closeRecognition();listening=false;
    micButton.classList.remove('listening');micButton.disabled=false;
    micLabel.textContent='Bấm micro rồi trả lời';feedback.textContent=error?.message||'Chưa kết nối được Azure Speech.';
  }
}
micButton?.addEventListener('click',startListening);

$('listenPrompt')?.addEventListener('click',()=>{const t=turns[current];play(t.prompt_audio,t.prompt);});
$('nextButton')?.addEventListener('click',()=>{if(!answered)return;if(current<turns.length-1){current++;render();}else finish();});

async function saveProgress(finalScore){
  if(isPreview||saving)return null;saving=true;
  try{
    const r=await 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({activity_id:activityId,score:finalScore,duration:Date.now()-startedAt,completed:finalScore>=Number(settings.passing_score||80),passing_score:Number(settings.passing_score||80),preview:false})});
    const j=await r.json().catch(()=>null);if(!r.ok||!j||j.ok===false)throw new Error(j?.message||'Save failed');return j.data||j;
  }catch(e){console.warn('Dialogue progress save failed:',e);return null;}finally{saving=false;}
}
function showPopup(result,finalScore){
  if(isPreview||!result)return;
  const xp=Number(result.xp_added||0), type=String(result.xp_event_type||'');
  $('xpPopupTitle').textContent=finalScore>=Number(settings.passing_score||80)?'Great!':'😊 Good effort!';
  if(xp>0){
    $('xpPopupScore').textContent=`+${xp} XP`;
    $('xpPopupText').textContent=type==='completion'?'Completion Reward':(type==='new_best'?'New Best Reward':'Practice Reward');
  }else{
    $('xpPopupScore').textContent=`${finalScore}%`;
    $('xpPopupText').textContent=finalScore>=Number(settings.passing_score||80)?'':'Try again!';
  }
  $('xpPopup').classList.add('show');
}
$('xpPopupButton')?.addEventListener('click',()=>$('xpPopup').classList.remove('show'));

async function finish(){
  const s=score(), pass=s>=Number(settings.passing_score||80);
  $('finalScore').textContent=`${s}/100`;$('finalPass').textContent=pass?'Yes':'No';
  $('finishTitle').textContent=pass?'Great!':'Keep going!';$('finishSubtitle').textContent=pass?'Dialogue complete.':'Try again.';
  continueButton.style.display=pass?'grid':'none';
  gameScreen.style.display='none';finishScreen.classList.add('show');
  const result=await saveProgress(s);showPopup(result,s);
}
$('againButton')?.addEventListener('click',()=>{current=0;turnScores=[];answered=false;hadWrong=false;saving=false;startedAt=Date.now();if(recognition&&listening){closeRecognition();listening=false;}$('xpPopup').classList.remove('show');finishScreen.classList.remove('show');gameScreen.style.display='block';render();});
render();
</script>
<?php endif; ?>
</body>
</html>
