<?php
declare(strict_types=1);

/**
 * EnglishSpeak - Azure Pronunciation Assessment API
 * File: api/pronunciation-assessment.php
 *
 * POST multipart/form-data:
 * - audio: recorded audio file
 * - reference_text: target word or sentence
 * - language: optional, defaults to en-US
 *
 * Response: JSON only.
 */

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

header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('X-Content-Type-Options: nosniff');

function paRespond(int $status, array $payload): never
{
    http_response_code($status);
    echo json_encode(
        $payload,
        JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
    );
    exit;
}

function paColumns(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 paAudioFormatFromUpload(array $file): array
{
    $tmpName = (string)($file['tmp_name'] ?? '');
    $reported = strtolower(trim((string)($file['type'] ?? '')));

    $detected = '';
    if ($tmpName !== '' && is_file($tmpName) && function_exists('finfo_open')) {
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        if ($finfo !== false) {
            $detected = strtolower((string)finfo_file($finfo, $tmpName));
            finfo_close($finfo);
        }
    }

    $candidate = $detected !== '' ? $detected : $reported;

    $formats = [
        'audio/wav' => ['kind' => 'wav', 'azure_type' => 'audio/wav; codecs=audio/pcm; samplerate=16000'],
        'audio/x-wav' => ['kind' => 'wav', 'azure_type' => 'audio/wav; codecs=audio/pcm; samplerate=16000'],
        'audio/wave' => ['kind' => 'wav', 'azure_type' => 'audio/wav; codecs=audio/pcm; samplerate=16000'],
        'audio/vnd.wave' => ['kind' => 'wav', 'azure_type' => 'audio/wav; codecs=audio/pcm; samplerate=16000'],
        'audio/ogg' => ['kind' => 'ogg', 'azure_type' => 'audio/ogg; codecs=opus'],
        'application/ogg' => ['kind' => 'ogg', 'azure_type' => 'audio/ogg; codecs=opus'],
        'audio/webm' => ['kind' => 'webm', 'azure_type' => ''],
        'video/webm' => ['kind' => 'webm', 'azure_type' => ''],
        'audio/mpeg' => ['kind' => 'mp3', 'azure_type' => ''],
        'audio/mp3' => ['kind' => 'mp3', 'azure_type' => ''],
    ];

    return $formats[$candidate] ?? ['kind' => '', 'azure_type' => ''];
}

function paFindFfmpeg(array $config): string
{
    $configured = trim((string)($config['ffmpeg_path'] ?? ''));

    $candidates = array_filter([
        $configured,
        'ffmpeg',
        'C:\\ffmpeg\\bin\\ffmpeg.exe',
        'C:\\laragon\\bin\\ffmpeg\\ffmpeg.exe',
        'C:\\laragon\\bin\\ffmpeg\\bin\\ffmpeg.exe',
    ]);

    foreach ($candidates as $candidate) {
        if ($candidate === 'ffmpeg') {
            $output = [];
            $status = 1;
            @exec('ffmpeg -version 2>&1', $output, $status);

            if ($status === 0) {
                return 'ffmpeg';
            }

            continue;
        }

        if (is_file($candidate)) {
            return $candidate;
        }
    }

    return '';
}

function paConvertToAzureWav(string $source, string $ffmpeg): string
{
    $target = tempnam(sys_get_temp_dir(), 'es_pa_');

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

    @unlink($target);
    $target .= '.wav';

    $command = escapeshellarg($ffmpeg)
        . ' -y -i ' . escapeshellarg($source)
        . ' -ac 1 -ar 16000 -c:a pcm_s16le '
        . escapeshellarg($target)
        . ' 2>&1';

    $output = [];
    $status = 1;
    @exec($command, $output, $status);

    if ($status !== 0 || !is_file($target) || filesize($target) <= 44) {
        @unlink($target);
        return '';
    }

    return $target;
}

function paSafeLanguage(string $language): string
{
    $language = trim($language);

    $allowed = [
        'en-US',
        'en-GB',
        'en-AU',
        'en-CA',
        'en-IN',
        'en-IE',
        'en-NZ',
    ];

    return in_array($language, $allowed, true) ? $language : 'en-US';
}

function paChildFeedback(
    float $pronunciation,
    float $accuracy,
    float $fluency,
    float $completeness,
    array $words
): array {
    $lowestWord = null;

    foreach ($words as $word) {
        $score = (float)($word['accuracyScore'] ?? 100);

        if ($lowestWord === null || $score < (float)$lowestWord['accuracyScore']) {
            $lowestWord = $word;
        }
    }

    if ($pronunciation >= 90 && $accuracy >= 90) {
        return [
            'level' => 'excellent',
            'title' => 'Tuyệt lắm!',
            'message' => 'Em phát âm rất rõ và chính xác. Chúng ta tiếp tục nhé!',
            'focus' => null,
        ];
    }

    if ($completeness < 80) {
        return [
            'level' => 'try_again',
            'title' => 'Em đã cố gắng rất tốt!',
            'message' => 'Em hãy đọc đủ từ hoặc cả câu, chậm rãi từ đầu đến cuối rồi thử lại nhé.',
            'focus' => 'completeness',
        ];
    }

    if ($fluency < 70) {
        return [
            'level' => 'try_again',
            'title' => 'Em đọc gần đúng rồi!',
            'message' => 'Em nghe lại mẫu, sau đó đọc liền mạch và chậm hơn một chút nhé.',
            'focus' => 'fluency',
        ];
    }

    if ($lowestWord !== null && (float)$lowestWord['accuracyScore'] < 75) {
        $wordText = trim((string)($lowestWord['word'] ?? ''));

        return [
            'level' => 'try_again',
            'title' => 'Em làm tốt lắm!',
            'message' => $wordText !== ''
                ? 'Từ “' . $wordText . '” chưa thật rõ. Em nghe lại từ mẫu, đọc chậm từng âm rồi thử lại nhé.'
                : 'Có một âm chưa thật rõ. Em nghe lại mẫu, đọc chậm từng âm rồi thử lại nhé.',
            'focus' => $wordText !== '' ? $wordText : 'accuracy',
        ];
    }

    return [
        'level' => 'good',
        'title' => 'Em đọc gần đúng rồi!',
        'message' => 'Em nghe lại mẫu một lần nữa, đọc chậm và rõ hơn rồi thử lại nhé. Em sẽ làm được!',
        'focus' => 'accuracy',
    ];
}

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    paRespond(405, [
        'ok' => false,
        'error' => 'METHOD_NOT_ALLOWED',
        'message' => 'API chỉ chấp nhận phương thức POST.',
    ]);
}

/*
 * Preview của giáo viên hoặc phiên học sinh đều được phép gọi API.
 * Không cho phép truy cập ẩn danh.
 */
$teacherPreview = !empty($_SESSION['user_id'])
    && in_array(
        (string)($_SESSION['user_role'] ?? ''),
        ['teacher', 'admin'],
        true
    );

$studentLoggedIn = !empty($_SESSION['student_id']);

if (!$teacherPreview && !$studentLoggedIn) {
    paRespond(401, [
        'ok' => false,
        'error' => 'AUTH_REQUIRED',
        'message' => 'Vui lòng đăng nhập trước khi luyện phát âm.',
    ]);
}

$referenceText = trim((string)($_POST['reference_text'] ?? ''));
$language = paSafeLanguage((string)($_POST['language'] ?? 'en-US'));

if ($referenceText === '') {
    paRespond(422, [
        'ok' => false,
        'error' => 'REFERENCE_TEXT_REQUIRED',
        'message' => 'Thiếu từ hoặc câu mẫu cần chấm.',
    ]);
}

if (mb_strlen($referenceText) > 500) {
    paRespond(422, [
        'ok' => false,
        'error' => 'REFERENCE_TEXT_TOO_LONG',
        'message' => 'Nội dung cần chấm quá dài.',
    ]);
}

if (!isset($_FILES['audio']) || !is_array($_FILES['audio'])) {
    paRespond(422, [
        'ok' => false,
        'error' => 'AUDIO_REQUIRED',
        'message' => 'Chưa nhận được file ghi âm.',
    ]);
}

$audio = $_FILES['audio'];
$uploadError = (int)($audio['error'] ?? UPLOAD_ERR_NO_FILE);

if ($uploadError !== UPLOAD_ERR_OK) {
    paRespond(422, [
        'ok' => false,
        'error' => 'AUDIO_UPLOAD_FAILED',
        'message' => 'File ghi âm chưa được tải lên thành công.',
        'upload_error' => $uploadError,
    ]);
}

$tmpName = (string)($audio['tmp_name'] ?? '');
$fileSize = (int)($audio['size'] ?? 0);

if ($tmpName === '' || !is_uploaded_file($tmpName)) {
    paRespond(422, [
        'ok' => false,
        'error' => 'INVALID_AUDIO_UPLOAD',
        'message' => 'File ghi âm không hợp lệ.',
    ]);
}

if ($fileSize <= 0 || $fileSize > 8 * 1024 * 1024) {
    paRespond(422, [
        'ok' => false,
        'error' => 'AUDIO_SIZE_INVALID',
        'message' => 'File ghi âm phải nhỏ hơn 8 MB.',
    ]);
}

$audioFormat = paAudioFormatFromUpload($audio);

if ($audioFormat['kind'] === '') {
    paRespond(415, [
        'ok' => false,
        'error' => 'AUDIO_FORMAT_NOT_SUPPORTED',
        'message' => 'Định dạng ghi âm chưa được hỗ trợ.',
    ]);
}

$configFile = __DIR__ . '/../config/azure_speech.php';

if (!is_file($configFile)) {
    paRespond(500, [
        'ok' => false,
        'error' => 'AZURE_CONFIG_MISSING',
        'message' => 'Chưa tìm thấy cấu hình Azure Speech.',
    ]);
}

$config = require $configFile;
$key = trim((string)($config['key'] ?? ''));
$region = strtolower(trim((string)($config['region'] ?? '')));

if ($key === '' || $region === '') {
    paRespond(500, [
        'ok' => false,
        'error' => 'AZURE_CONFIG_INVALID',
        'message' => 'Azure Speech Key hoặc Region chưa hợp lệ.',
    ]);
}

$convertedFile = '';

if (in_array($audioFormat['kind'], ['webm', 'mp3'], true)) {
    $ffmpeg = paFindFfmpeg($config);

    if ($ffmpeg === '') {
        paRespond(415, [
            'ok' => false,
            'error' => 'AUDIO_CONVERSION_REQUIRED',
            'message' => 'Trình duyệt đang ghi âm WebM. Máy chủ cần FFmpeg hoặc trang học cần ghi âm WAV.',
            'detected_format' => $audioFormat['kind'],
        ]);
    }

    $convertedFile = paConvertToAzureWav($tmpName, $ffmpeg);

    if ($convertedFile === '') {
        paRespond(422, [
            'ok' => false,
            'error' => 'AUDIO_CONVERSION_FAILED',
            'message' => 'Không chuyển được file ghi âm sang WAV 16 kHz mono.',
        ]);
    }

    $tmpName = $convertedFile;
    $audioFormat = [
        'kind' => 'wav',
        'azure_type' => 'audio/wav; codecs=audio/pcm; samplerate=16000',
    ];
}

$contentType = (string)$audioFormat['azure_type'];

$audioBytes = file_get_contents($tmpName);

if ($audioBytes === false || $audioBytes === '') {
    paRespond(422, [
        'ok' => false,
        'error' => 'AUDIO_READ_FAILED',
        'message' => 'Không đọc được file ghi âm.',
    ]);
}

$assessmentConfig = [
    'ReferenceText' => $referenceText,
    'GradingSystem' => 'HundredMark',
    'Granularity' => 'Phoneme',
    'Dimension' => 'Comprehensive',
    'EnableMiscue' => true,
];

$assessmentHeader = base64_encode(
    json_encode(
        $assessmentConfig,
        JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
    )
);

$endpoint = sprintf(
    'https://%s.stt.speech.microsoft.com/speech/recognition/conversation/cognitiveservices/v1?%s',
    rawurlencode($region),
    http_build_query([
        'language' => $language,
        'format' => 'detailed',
    ], '', '&', PHP_QUERY_RFC3986)
);

$ch = curl_init($endpoint);

if ($ch === false) {
    paRespond(500, [
        'ok' => false,
        'error' => 'CURL_INIT_FAILED',
        'message' => 'Không thể khởi tạo kết nối Azure Speech.',
    ]);
}

curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $audioBytes,
    CURLOPT_HTTPHEADER => [
        'Ocp-Apim-Subscription-Key: ' . $key,
        'Content-Type: ' . $contentType,
        'Accept: application/json',
        'Pronunciation-Assessment: ' . $assessmentHeader,
        'Content-Length: ' . strlen($audioBytes),
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT => 35,
    CURLOPT_SSL_VERIFYPEER => true,
    CURLOPT_SSL_VERIFYHOST => 2,
]);

$rawResponse = curl_exec($ch);
$curlError = curl_error($ch);
$httpStatus = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);

curl_close($ch);

if ($convertedFile !== '') {
    @unlink($convertedFile);
}

if ($rawResponse === false) {
    paRespond(502, [
        'ok' => false,
        'error' => 'AZURE_CONNECTION_FAILED',
        'message' => 'Không kết nối được Azure Speech. Em hãy thử lại sau.',
        'detail' => $curlError,
    ]);
}

$azure = json_decode((string)$rawResponse, true);

if (!is_array($azure)) {
    paRespond(502, [
        'ok' => false,
        'error' => 'AZURE_RESPONSE_INVALID',
        'message' => 'Azure Speech trả về dữ liệu không hợp lệ.',
        'http_status' => $httpStatus,
        'curl_error' => $curlError,
        'endpoint' => $endpoint,
        'content_type' => $contentType,
        'raw_response' => (string)$rawResponse,
    ]);
}

if ($httpStatus < 200 || $httpStatus >= 300) {
    paRespond(502, [
        'ok' => false,
        'error' => 'AZURE_REQUEST_FAILED',
        'message' => 'Azure Speech chưa chấm được file ghi âm.',
        'http_status' => $httpStatus,
        'curl_error' => $curlError,
        'endpoint' => $endpoint,
        'content_type' => $contentType,
        'azure' => $azure,
        'raw_response' => (string)$rawResponse,
    ]);
}

$recognitionStatus = (string)($azure['RecognitionStatus'] ?? '');

if ($recognitionStatus !== 'Success') {
    paRespond(422, [
        'ok' => false,
        'error' => 'SPEECH_NOT_RECOGNIZED',
        'message' => 'Chưa nghe rõ giọng nói của em. Em hãy nói gần micro và thử lại nhé.',
        'recognition_status' => $recognitionStatus,
    ]);
}

$best = is_array($azure['NBest'][0] ?? null)
    ? $azure['NBest'][0]
    : [];

$assessment = is_array($best['PronunciationAssessment'] ?? null)
    ? $best['PronunciationAssessment']
    : [];

$wordResults = [];

foreach (($best['Words'] ?? []) as $wordRow) {
    if (!is_array($wordRow)) {
        continue;
    }

    $wordAssessment = is_array($wordRow['PronunciationAssessment'] ?? null)
        ? $wordRow['PronunciationAssessment']
        : [];

    $phonemes = [];

    foreach (($wordRow['Phonemes'] ?? []) as $phonemeRow) {
        if (!is_array($phonemeRow)) {
            continue;
        }

        $phonemeAssessment = is_array(
            $phonemeRow['PronunciationAssessment'] ?? null
        )
            ? $phonemeRow['PronunciationAssessment']
            : [];

        $phonemes[] = [
            'phoneme' => (string)($phonemeRow['Phoneme'] ?? ''),
            'accuracyScore' => round(
                (float)($phonemeAssessment['AccuracyScore'] ?? 0),
                1
            ),
        ];
    }

    $wordResults[] = [
        'word' => (string)($wordRow['Word'] ?? ''),
        'accuracyScore' => round(
            (float)($wordAssessment['AccuracyScore'] ?? 0),
            1
        ),
        'errorType' => (string)($wordAssessment['ErrorType'] ?? 'None'),
        'phonemes' => $phonemes,
    ];
}

$accuracy = round((float)($assessment['AccuracyScore'] ?? 0), 1);
$fluency = round((float)($assessment['FluencyScore'] ?? 0), 1);
$completeness = round((float)($assessment['CompletenessScore'] ?? 0), 1);
$pronunciation = round(
    (float)(
        $assessment['PronScore']
        ?? $assessment['PronunciationScore']
        ?? 0
    ),
    1
);

$feedback = paChildFeedback(
    $pronunciation,
    $accuracy,
    $fluency,
    $completeness,
    $wordResults
);

paRespond(200, [
    'ok' => true,
    'referenceText' => $referenceText,
    'recognizedText' => (string)(
        $best['Display']
        ?? $azure['DisplayText']
        ?? ''
    ),
    'scores' => [
        'pronunciation' => $pronunciation,
        'accuracy' => $accuracy,
        'fluency' => $fluency,
        'completeness' => $completeness,
    ],
    'words' => $wordResults,
    'feedback' => $feedback,
    'meta' => [
        'language' => $language,
        'activityId' => (int)($_POST['activity_id'] ?? 0),
        'preview' => $teacherPreview && !$studentLoggedIn,
    ],
]);
