<?php
// =============================================================
//  services/GeminiChatbotService.php
//
//  Extracted from app/nurse/nurse_ai_proxy.php and
//  app/student/student_ai_proxy.php, which were byte-for-byte identical
//  in structure (message sanitisation, Gemini contents-building, the
//  model-fallback call, response parsing) and differed only in which
//  tables their "gather this person's own data" section queried. That
//  made them a clean, low-risk extraction — a mechanical split into
//  generic helpers plus one context-builder per role — unlike the
//  staff-side app/ai_proxy.php (747 lines, system-wide data across every
//  nurse/student/department), which is NOT touched here: the thesis's own
//  mobile scope keeps the chatbot Nurse/Student-first (Admin/Preceptor
//  chatbot access is explicitly "optional"), so that's the safer file to
//  leave exactly as it already works in production rather than rushing
//  its much larger refactor into this same pass.
//
//  app/nurse/nurse_ai_proxy.php and app/student/student_ai_proxy.php now
//  both call the functions here instead of carrying their own copies —
//  same output, same prompts, same DB queries, verified against the
//  pre-refactor behaviour before this replaced it. api/v1/chatbot/ask.php
//  (the mobile endpoint) is built on the exact same functions.
// =============================================================

require_once __DIR__ . '/../include/config.php'; // GEMINI_API_KEY

// ── DB helpers (identical small wrappers both original proxies had) ──────────
function gemini_db_val(mysqli $conn, string $sql)
{
    try {
        $r = $conn->query($sql);
        if (!$r) return 0;
        $row = $r->fetch_row();
        return $row ? $row[0] : 0;
    } catch (Throwable $e) {
        error_log('GeminiChatbotService: db_val failed — ' . $e->getMessage());
        return 0;
    }
}

function gemini_db_rows(mysqli $conn, string $sql): array
{
    try {
        $r = $conn->query($sql);
        if (!$r) return [];
        return $r->fetch_all(MYSQLI_ASSOC);
    } catch (Throwable $e) {
        error_log('GeminiChatbotService: db_rows failed — ' . $e->getMessage());
        return [];
    }
}

function gemini_db_rows_p(mysqli $conn, string $sql, string $types, array $params): array
{
    try {
        $stmt = $conn->prepare($sql);
        if (!$stmt) return [];
        $stmt->bind_param($types, ...$params);
        $stmt->execute();
        $rows = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
        $stmt->close();
        return $rows;
    } catch (Throwable $e) {
        error_log('GeminiChatbotService: db_rows_p failed — ' . $e->getMessage());
        return [];
    }
}

/**
 * Validates/trims a raw client-supplied message list down to the last 12
 * user/assistant turns, each capped at 4000 chars — same budget both
 * original proxies used to keep token usage and stale-context risk low
 * for a personal-assistant use case.
 */
function sanitizeChatMessages(array $rawMessages): array
{
    $sanitized = [];
    foreach ($rawMessages as $msg) {
        $role    = isset($msg['role'])    ? (string)$msg['role']    : '';
        $content = isset($msg['content']) ? (string)$msg['content'] : '';
        if (in_array($role, ['user', 'assistant'], true) && $content !== '' && mb_strlen($content) <= 4000) {
            $sanitized[] = ['role' => $role, 'content' => trim($content)];
        }
    }
    if (count($sanitized) > 12) {
        $sanitized = array_slice($sanitized, -12);
    }
    return $sanitized;
}

/** Turns a sanitized message list into Gemini's contents[] shape, merging consecutive same-role turns. */
function buildGeminiContents(array $sanitizedMessages): array
{
    $contents  = [];
    $prevRole  = null;
    foreach ($sanitizedMessages as $msg) {
        $g = ($msg['role'] === 'assistant') ? 'model' : 'user';
        if ($g === $prevRole && !empty($contents)) {
            $contents[count($contents) - 1]['parts'][0]['text'] .= "\n" . $msg['content'];
        } else {
            $contents[] = ['role' => $g, 'parts' => [['text' => $msg['content']]]];
            $prevRole = $g;
        }
    }
    if (!empty($contents) && $contents[count($contents) - 1]['role'] === 'model') {
        $contents[] = ['role' => 'user', 'parts' => [['text' => 'Please continue.']]];
    }
    return $contents;
}

/**
 * Calls Gemini with the same model-fallback chain (Flash → Pro →
 * Flash-Lite, skipping past quota-exhausted models) both original proxies
 * used. Side-effect-free — returns a result array instead of exiting, so
 * callers (a web page's ai_error(), or the API's Api::error()) decide how
 * to surface a failure.
 *
 * @return array{ok: bool, text: ?string, model: ?string, input_tokens: int, output_tokens: int, error: ?string, http_code: int, safety_blocked: bool}
 */
function callGemini(string $systemPrompt, array $geminiContents): array
{
    if (!defined('GEMINI_API_KEY') || strlen(GEMINI_API_KEY) < 20) {
        return ['ok' => false, 'text' => null, 'model' => null, 'input_tokens' => 0, 'output_tokens' => 0,
                'error' => 'GEMINI_API_KEY not set', 'http_code' => 500, 'safety_blocked' => false];
    }

    $payload = [
        'contents'          => $geminiContents,
        'generationConfig'  => ['maxOutputTokens' => 1024, 'temperature' => 0.6, 'topP' => 0.9],
        'systemInstruction' => ['parts' => [['text' => $systemPrompt]]],
    ];

    $modelsToTry = ['gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-2.5-flash-lite'];
    $result = null; $httpCode = 0; $curlErr = ''; $model = '';

    foreach ($modelsToTry as $tryModel) {
        $apiUrl = 'https://generativelanguage.googleapis.com/v1beta/models/'
                . $tryModel . ':generateContent?key=' . GEMINI_API_KEY;

        $ch = curl_init($apiUrl);
        curl_setopt_array($ch, [
            CURLOPT_POST           => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POSTFIELDS     => json_encode($payload),
            CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
            CURLOPT_TIMEOUT        => 45,
            CURLOPT_CONNECTTIMEOUT => 15,
        ]);
        $result   = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlErr  = curl_error($ch);
        curl_close($ch);

        if ($curlErr) { error_log("GeminiChatbotService: cURL error for {$tryModel}: {$curlErr}"); continue; }
        if ($httpCode === 200) { $model = $tryModel; break; }

        $errCheck  = json_decode((string)$result, true);
        $errStatus = $errCheck['error']['status']  ?? '';
        $errMsg    = $errCheck['error']['message'] ?? '';

        if ($errStatus === 'RESOURCE_EXHAUSTED' || stripos($errMsg, 'quota') !== false) {
            error_log("GeminiChatbotService: quota exhausted for {$tryModel}, trying next...");
            continue;
        }
        error_log("GeminiChatbotService: non-recoverable error for {$tryModel} (HTTP {$httpCode}): {$errMsg}");
        break;
    }

    if ($curlErr && !$result) {
        return ['ok' => false, 'text' => null, 'model' => null, 'input_tokens' => 0, 'output_tokens' => 0,
                'error' => 'Network error: ' . $curlErr, 'http_code' => 502, 'safety_blocked' => false];
    }
    if ($httpCode !== 200) {
        $e = json_decode((string)$result, true);
        return ['ok' => false, 'text' => null, 'model' => null, 'input_tokens' => 0, 'output_tokens' => 0,
                'error' => $e['error']['message'] ?? ('Gemini HTTP ' . $httpCode), 'http_code' => 502, 'safety_blocked' => false];
    }

    $data = json_decode((string)$result, true);
    $text = $data['candidates'][0]['content']['parts'][0]['text'] ?? '';

    if ($text === '') {
        $fin = $data['candidates'][0]['finishReason'] ?? 'UNKNOWN';
        if ($fin === 'SAFETY') {
            return ['ok' => true, 'text' => 'Cannot respond due to safety guidelines. Please rephrase.', 'model' => $model,
                     'input_tokens' => 0, 'output_tokens' => 0, 'error' => null, 'http_code' => 200, 'safety_blocked' => true];
        }
        return ['ok' => false, 'text' => null, 'model' => null, 'input_tokens' => 0, 'output_tokens' => 0,
                'error' => 'Empty Gemini response (reason: ' . $fin . ')', 'http_code' => 500, 'safety_blocked' => false];
    }

    return [
        'ok' => true, 'text' => $text, 'model' => $model,
        'input_tokens'  => $data['usageMetadata']['promptTokenCount']     ?? 0,
        'output_tokens' => $data['usageMetadata']['candidatesTokenCount'] ?? 0,
        'error' => null, 'http_code' => 200, 'safety_blocked' => false,
    ];
}

/**
 * Builds the Rotation Nurse chatbot's system prompt — identical queries
 * and prompt text to nurse_ai_proxy.php's original inline version.
 * @return array{base_system: string, live_data: string, system: string}|null null if the nurse's profile isn't found (caller should 404).
 *   'system' is base_system + live_data pre-joined for callers that don't need to insert anything between them.
 */
function buildNurseChatContext(mysqli $conn, int $nurseId): ?array
{
    $profileRows = gemini_db_rows_p($conn, "
        SELECT firstname, surname, school_name, specialty_name, rotation_status, student_id
        FROM rotation_nurses WHERE id = ? AND status = 'Active' LIMIT 1
    ", 'i', [$nurseId]);
    if (empty($profileRows)) return null;
    $me = $profileRows[0];

    $currentRotation = gemini_db_rows_p($conn, "
        SELECT nr.status, nr.start_date,
               d.department_name, COALESCE(u.unit_name, 'Unassigned unit') AS unit_name
        FROM nurse_rotations nr
        LEFT JOIN departments d ON nr.department_id = d.id
        LEFT JOIN units u ON nr.unit_id = u.id
        WHERE nr.nurse_id = ? AND nr.status = 'Inprogress'
        LIMIT 1
    ", 'i', [$nurseId]);

    $mySchedule = gemini_db_rows_p($conn, "
        SELECT nrs.department_name,
               COALESCE(u.unit_name, nrs.unit_name, CONCAT('Unit #', nrs.unit_id)) AS unit_name,
               nrs.start_week, nrs.end_week, nrs.status AS schedule_status,
               DATE_ADD(nr.start_date, INTERVAL (nrs.start_week - 1) WEEK) AS scheduled_start_date,
               DATE_ADD(nr.start_date, INTERVAL nrs.end_week WEEK)         AS scheduled_end_date
        FROM nurse_rotation_schedules nrs
        JOIN nurse_rotations nr ON nrs.nurse_rotation_id = nr.id
        WHERE nrs.rotation_nurse_id = ?
        ORDER BY nrs.start_week ASC
    ", 'i', [$nurseId]);

    $myAttendance = gemini_db_rows_p($conn, "
        SELECT attendance_date, status, remarks, unit_name
        FROM nurse_attendance
        WHERE rotation_nurse_id = ?
          AND attendance_date >= DATE_SUB(CURDATE(), INTERVAL 60 DAY)
        ORDER BY attendance_date DESC
        LIMIT 60
    ", 'i', [$nurseId]);
    $attTotal   = count($myAttendance);
    $attPresent = count(array_filter($myAttendance, fn($a) => $a['status'] === 'Present'));
    $attRate    = $attTotal > 0 ? round(($attPresent / $attTotal) * 100, 1) : 0;

    $myAssessments = gemini_db_rows_p($conn, "
        SELECT stage1_status, s1_percentage, s1_grade,
               stage2_status, s2_percentage, s2_grade,
               stage3_status, s3_percentage, s3_grade,
               final_percentage, final_grade, final_verdict, overall_status
        FROM nurse_assessments
        WHERE rotation_nurse_id = ?
        ORDER BY created_at DESC
    ", 'i', [$nurseId]);

    $noticeTableExists = (bool)gemini_db_val($conn, "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'notices'");
    $relevantNotices = $noticeTableExists ? gemini_db_rows($conn, "
        SELECT title, priority, created_at
        FROM notices
        WHERE audience IN ('nurses','both') AND status = 'active' AND (expires_at IS NULL OR expires_at >= CURDATE())
        ORDER BY is_pinned DESC, created_at DESC
        LIMIT 8
    ") : [];

    $today   = date('l, d F Y');
    $myName  = trim($me['firstname'] . ' ' . $me['surname']);

    $liveData = "\n=== YOUR DATA (as of {$today}) ===\n"
              . "You are chatting with: {$myName} (Rotation Nurse, #{$me['student_id']})\n"
              . "Specialty: {$me['specialty_name']} | School: {$me['school_name']}\n"
              . "Overall rotation status: {$me['rotation_status']}\n\n"
              . "--- CURRENT PLACEMENT ---\n";

    if (!empty($currentRotation)) {
        $cr = $currentRotation[0];
        $liveData .= "Currently at: {$cr['department_name']} / {$cr['unit_name']} (started {$cr['start_date']})\n";
    } else {
        $liveData .= "Not currently placed in an active rotation.\n";
    }

    $liveData .= "\n--- MY FULL ROTATION SCHEDULE ---\n";
    foreach ($mySchedule as $step) {
        $liveData .= "Week {$step['start_week']}-{$step['end_week']} [{$step['schedule_status']}]: "
                   . "{$step['department_name']} / {$step['unit_name']} "
                   . "({$step['scheduled_start_date']} to {$step['scheduled_end_date']})\n";
    }
    if (empty($mySchedule)) $liveData .= "(No schedule steps found yet)\n";

    $liveData .= "\n--- MY ATTENDANCE (last 60 days) ---\n";
    $liveData .= "Attendance rate: {$attRate}% ({$attPresent}/{$attTotal} present)\n";
    foreach (array_slice($myAttendance, 0, 15) as $a) {
        $liveData .= "{$a['attendance_date']}: {$a['status']} at {$a['unit_name']}" . ($a['remarks'] ? " — {$a['remarks']}" : '') . "\n";
    }
    if (empty($myAttendance)) $liveData .= "(No attendance records found)\n";

    $liveData .= "\n--- MY ASSESSMENTS ---\n";
    foreach ($myAssessments as $a) {
        $liveData .= "Overall status: {$a['overall_status']}\n";
        if ($a['s1_percentage'] !== null) $liveData .= "  Stage 1: {$a['s1_percentage']}% ({$a['s1_grade']}) — {$a['stage1_status']}\n";
        if ($a['s2_percentage'] !== null) $liveData .= "  Stage 2: {$a['s2_percentage']}% ({$a['s2_grade']}) — {$a['stage2_status']}\n";
        if ($a['s3_percentage'] !== null) $liveData .= "  Stage 3: {$a['s3_percentage']}% ({$a['s3_grade']}) — {$a['stage3_status']}\n";
        if ($a['final_percentage'] !== null) $liveData .= "  FINAL: {$a['final_percentage']}% — {$a['final_grade']} — Verdict: {$a['final_verdict']}\n";
    }
    if (empty($myAssessments)) $liveData .= "(No assessments recorded yet)\n";

    $liveData .= "\n--- NOTICES FOR YOU ---\n";
    foreach ($relevantNotices as $n) {
        $liveData .= "[{$n['priority']}] {$n['title']} ({$n['created_at']})\n";
    }
    if (empty($relevantNotices)) $liveData .= "(No active notices right now)\n";
    $liveData .= "\n=== END OF YOUR DATA ===\n";

    $baseSystem = "You are a personal AI assistant for a Rotation Nurse using STH NRMS "
                . "(Sunyani Teaching Hospital Nurse Rotation Management System) in Ghana. "
                . "You can ONLY discuss the logged-in nurse's own rotation schedule, attendance, "
                . "assessments, and notices addressed to nurses — shown below. "
                . "You have NO access to and must NEVER speculate about other nurses, clinical "
                . "students, staff, or any system-wide data. If asked about anyone else's "
                . "information, politely explain you can only help with their own data. "
                . "Be warm, concise, and encouraging. Use Ghanaian English (use 'kindly' not 'please').";

    return ['base_system' => $baseSystem, 'live_data' => $liveData, 'system' => $baseSystem . "\n\n" . $liveData];
}

/**
 * Builds the Clinical Student chatbot's system prompt — identical queries
 * and prompt text to student_ai_proxy.php's original inline version.
 * @return array{base_system: string, live_data: string, system: string}|null null if the student's profile isn't found (caller should 404).
 *   'system' is base_system + live_data pre-joined for callers that don't need to insert anything between them.
 */
function buildStudentChatContext(mysqli $conn, int $studentId): ?array
{
    $profileRows = gemini_db_rows_p($conn, "
        SELECT firstname, surname, school_name, level, programme, rotation_status, student_id
        FROM clinical_students WHERE id = ? AND status = 'Active' LIMIT 1
    ", 'i', [$studentId]);
    if (empty($profileRows)) return null;
    $me = $profileRows[0];

    $currentBlock = gemini_db_rows_p($conn, "
        SELECT srd.unit_name, srd.supervisor_name, MIN(srd.rotation_date) AS block_start, MAX(srd.rotation_date) AS block_end
        FROM student_rotation_details srd
        WHERE srd.clinical_student_id = ? AND srd.status = 'Ongoing'
        GROUP BY srd.unit_id
        LIMIT 1
    ", 'i', [$studentId]);

    $myBlocks = gemini_db_rows_p($conn, "
        SELECT srd.unit_name, srd.supervisor_name,
               MIN(srd.rotation_date) AS block_start, MAX(srd.rotation_date) AS block_end,
               COUNT(*) AS total_days,
               SUM(CASE WHEN srd.status = 'Completed' THEN 1 ELSE 0 END) AS days_completed
        FROM student_rotation_details srd
        WHERE srd.clinical_student_id = ?
        GROUP BY srd.unit_id
        ORDER BY block_start ASC
    ", 'i', [$studentId]);

    $myAttendance = gemini_db_rows_p($conn, "
        SELECT attendance_date, status, remarks, unit_name
        FROM clinical_attendance
        WHERE clinical_student_id = ?
          AND attendance_date >= DATE_SUB(CURDATE(), INTERVAL 60 DAY)
        ORDER BY attendance_date DESC
        LIMIT 60
    ", 'i', [$studentId]);
    $attTotal   = count($myAttendance);
    $attPresent = count(array_filter($myAttendance, fn($a) => $a['status'] === 'Present'));
    $attRate    = $attTotal > 0 ? round(($attPresent / $attTotal) * 100, 1) : 0;

    $myUnitAssessments = gemini_db_rows_p($conn, "
        SELECT unit_name, percentage_score, grade, eval_status, assessment_date
        FROM clinical_assessments
        WHERE clinical_student_id = ? AND eval_status IN ('submitted','locked')
        ORDER BY created_at DESC
    ", 'i', [$studentId]);

    $myFinal = gemini_db_rows_p($conn, "
        SELECT average_percentage, final_grade, final_verdict, preceptor_name
        FROM clinical_final_assessments
        WHERE clinical_student_id = ?
        ORDER BY created_at DESC LIMIT 1
    ", 'i', [$studentId]);

    $noticeTableExists = (bool)gemini_db_val($conn, "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'notices'");
    $relevantNotices = $noticeTableExists ? gemini_db_rows($conn, "
        SELECT title, priority, created_at
        FROM notices
        WHERE audience IN ('students','both') AND status = 'active' AND (expires_at IS NULL OR expires_at >= CURDATE())
        ORDER BY is_pinned DESC, created_at DESC
        LIMIT 8
    ") : [];

    $today  = date('l, d F Y');
    $myName = trim($me['firstname'] . ' ' . $me['surname']);

    $liveData = "\n=== YOUR DATA (as of {$today}) ===\n"
              . "You are chatting with: {$myName} (Clinical Student, #{$me['student_id']})\n"
              . "School: {$me['school_name']} | Level: {$me['level']} | Programme: {$me['programme']}\n"
              . "Overall rotation status: {$me['rotation_status']}\n\n"
              . "--- CURRENT PLACEMENT ---\n";

    if (!empty($currentBlock)) {
        $cb = $currentBlock[0];
        $liveData .= "Currently at: {$cb['unit_name']} (supervisor: " . ($cb['supervisor_name'] ?: 'N/A') . ") "
                   . "from {$cb['block_start']} to {$cb['block_end']}\n";
    } else {
        $liveData .= "Not currently placed in an active unit.\n";
    }

    $liveData .= "\n--- MY ROTATION BLOCK HISTORY ---\n";
    foreach ($myBlocks as $b) {
        $liveData .= "{$b['unit_name']} (supervisor: " . ($b['supervisor_name'] ?: 'N/A') . "): "
                   . "{$b['days_completed']}/{$b['total_days']} days completed "
                   . "({$b['block_start']} to {$b['block_end']})\n";
    }
    if (empty($myBlocks)) $liveData .= "(No rotation blocks found yet)\n";

    $liveData .= "\n--- MY ATTENDANCE (last 60 days) ---\n";
    $liveData .= "Attendance rate: {$attRate}% ({$attPresent}/{$attTotal} present)\n";
    foreach (array_slice($myAttendance, 0, 15) as $a) {
        $liveData .= "{$a['attendance_date']}: {$a['status']} at {$a['unit_name']}" . ($a['remarks'] ? " — {$a['remarks']}" : '') . "\n";
    }
    if (empty($myAttendance)) $liveData .= "(No attendance records found)\n";

    $liveData .= "\n--- MY UNIT ASSESSMENTS ---\n";
    foreach ($myUnitAssessments as $a) {
        $score = $a['percentage_score'] !== null ? round($a['percentage_score'], 1) . '%' : 'N/A';
        $liveData .= "{$a['unit_name']}: {$score} ({$a['grade']}) — {$a['eval_status']}" . ($a['assessment_date'] ? " on {$a['assessment_date']}" : '') . "\n";
    }
    if (empty($myUnitAssessments)) $liveData .= "(No unit assessments recorded yet)\n";

    $liveData .= "\n--- MY FINAL ASSESSMENT ---\n";
    if (!empty($myFinal)) {
        $f = $myFinal[0];
        $avg = $f['average_percentage'] !== null ? round($f['average_percentage'], 1) . '%' : 'N/A';
        $liveData .= "Average: {$avg} — Grade: {$f['final_grade']} — Verdict: {$f['final_verdict']}" . ($f['preceptor_name'] ? " — Preceptor: {$f['preceptor_name']}" : '') . "\n";
    } else {
        $liveData .= "(No final assessment recorded yet)\n";
    }

    $liveData .= "\n--- NOTICES FOR YOU ---\n";
    foreach ($relevantNotices as $n) {
        $liveData .= "[{$n['priority']}] {$n['title']} ({$n['created_at']})\n";
    }
    if (empty($relevantNotices)) $liveData .= "(No active notices right now)\n";
    $liveData .= "\n=== END OF YOUR DATA ===\n";

    $baseSystem = "You are a personal AI assistant for a Clinical Student using STH NRMS "
                . "(Sunyani Teaching Hospital Nurse Rotation Management System) in Ghana. "
                . "You can ONLY discuss the logged-in student's own rotation blocks, attendance, "
                . "assessments, and notices addressed to students — shown below. "
                . "You have NO access to and must NEVER speculate about other students, rotation "
                . "nurses, staff, or any system-wide data. If asked about anyone else's "
                . "information, politely explain you can only help with their own data. "
                . "Be warm, encouraging, and concise. Use Ghanaian English (use 'kindly' not 'please').";

    return ['base_system' => $baseSystem, 'live_data' => $liveData, 'system' => $baseSystem . "\n\n" . $liveData];
}
