<?php
declare(strict_types=1);

require_once __DIR__ . '/claude_api.php';

// RBC AI Engine — 4-provider cascade (Claude → Groq → Gemini → OpenAI)
if (!class_exists('RBCAIEngine')) {
    require_once dirname(__DIR__, 2) . '/ai/provider_engine.php';
}

// Token budget for a single question-generation call. Kept moderate so requests
// stay within provider limits: Groq's free tier counts max_tokens toward a 12K
// tokens-per-minute cap, and non-streaming Claude calls must finish within the
// cURL timeout. Large jobs are handled by splitting into batches (below), NOT by
// raising this ceiling. Gemini is clamped to its own 8192 output cap, and Groq
// lower still, inside the provider engine.
if (!defined('AI_QUESTION_MAX_TOKENS')) {
    define('AI_QUESTION_MAX_TOKENS', 8000);
}
// When a single call still truncates, the chunk is re-run in batches of at most
// this many questions each, so every response comfortably fits the budget.
if (!defined('AI_QUESTION_BATCH_SIZE')) {
    define('AI_QUESTION_BATCH_SIZE', 10);
}

/**
 * Split a chunk's requested question counts into smaller batches so each AI call
 * asks for at most $maxPerBatch questions. Every question type and the diagram
 * allowance are spread as evenly as possible across the batches.
 *
 * @return array<int,array{mcq:int,fill:int,theory:int,tf:int,match:int,comp:int,diag:int}>
 */
function splitQuestionCounts(
    int $mcq, int $fill, int $theory, int $tf, int $match, int $comp, int $diag,
    int $maxPerBatch
): array {
    $total = $mcq + $fill + $theory + $tf + $match + $comp;
    if ($total <= 0) {
        return [];
    }
    $maxPerBatch = max(1, $maxPerBatch);
    $batchCount  = (int) ceil($total / $maxPerBatch);

    // Spread $count items across $n batches as evenly as possible.
    $spread = static function (int $count, int $n): array {
        $base = intdiv($count, $n);
        $rem  = $count % $n;
        $out  = [];
        for ($i = 0; $i < $n; $i++) {
            $out[$i] = $base + ($i < $rem ? 1 : 0);
        }
        return $out;
    };

    $mcqB    = $spread($mcq, $batchCount);
    $fillB   = $spread($fill, $batchCount);
    $theoryB = $spread($theory, $batchCount);
    $tfB     = $spread($tf, $batchCount);
    $matchB  = $spread($match, $batchCount);
    $compB   = $spread($comp, $batchCount);
    $diagB   = $spread($diag, $batchCount);

    $batches = [];
    for ($i = 0; $i < $batchCount; $i++) {
        $batches[] = [
            'mcq'    => $mcqB[$i],
            'fill'   => $fillB[$i],
            'theory' => $theoryB[$i],
            'tf'     => $tfB[$i],
            'match'  => $matchB[$i],
            'comp'   => $compB[$i],
            'diag'   => $diagB[$i],
        ];
    }
    return $batches;
}

/**
 * Generate questions from a single content chunk and immediately INSERT them
 * into exam_questions.
 *
 * @param mysqli $conn
 * @param int    $subjectId
 * @param string $subjectCode
 * @param string $subjectName
 * @param string $className
 * @param string $term
 * @param string $academicYear
 * @param string $difficulty
 * @param int    $mcqCount
 * @param int    $theoryCount
 * @param int    $fillCount
 * @param int    $diagramCount
 * @param string $chunkContent   Cleaned text for this chunk
 * @param string $chunkTopic     Descriptive heading for this chunk
 * @param int    $jobId
 * @param int    $userId
 * @return array{inserted_ids:int[],engine:string,count:int,attempts:array}
 */
function generateChunkQuestions(
    mysqli $conn,
    int    $subjectId,
    string $subjectCode,
    string $subjectName,
    string $className,
    string $term,
    string $academicYear,
    string $difficulty,
    int    $mcqCount,
    int    $theoryCount,
    int    $fillCount,
    int    $diagramCount,
    string $chunkContent,
    string $chunkTopic,
    int    $jobId,
    int    $userId,
    bool   $fromCurriculum = false,
    int    $tfCount = 0,
    int    $matchingCount = 0,
    int    $comprehensionCount = 0
): array {

    // Source framing: study material (RAG) vs standard curriculum (topic mode).
    if ($fromCurriculum) {
        $sourceFraming   = "the standard Ghana Education Service (GES) curriculum for the given subject, class level, and term, covering the requested topics.";
        $sourcePrinciple = "6. Base every question on the standard GES curriculum for the stated subject/class/term and the listed topics; keep all facts accurate and age-appropriate.";
        $materialHeader  = "TOPICS TO COVER (generate from the standard curriculum; if a topic includes a weight like 'Fractions:40', give higher-weighted topics proportionally more questions):";
    } else {
        $sourceFraming   = "the provided study material ONLY — never from general knowledge outside it.";
        $sourcePrinciple = "6. Questions MUST come strictly from the provided study material. Do not invent facts.";
        $materialHeader  = "STUDY MATERIAL FOR THIS SECTION:";
    }

    // ── Exam-officer system prompt ─────────────────────────────────────────
    $system = <<<SYSTEM
You are an experienced examination officer for the Ghana Education Service (GES).
Your role is to set rigorous, fair, curriculum-aligned examination questions from
{$sourceFraming}

EXAM OFFICER PRINCIPLES:
1. Test understanding and application, not rote memorisation.
2. MCQ distractors MUST be plausible — they should reflect common student misconceptions,
   not obviously wrong answers. A student who has not studied cannot immediately rule them out.
3. Use language appropriate for the stated class level (JHS vs SHS vs Primary).
4. Fill-in-the-blank must target key vocabulary, formulas, or factual definitions — never trivial connecting words.
5. Theory questions must require demonstration of knowledge, reasoning, or explanation — not a one-word answer.
{$sourcePrinciple}
7. Marks allocation: MCQ = 1 mark, Fill = 1–2 marks, Theory = 3–10 marks (scale with complexity).
8. Avoid ambiguous, culturally biased, or trick questions.
9. For diagram questions: set diagram_required = true and provide a clear diagram_spec describing exactly what to draw.
10. Distribute questions across different topics within the provided content — do not cluster on one concept.
11. For TERM-specific questions: focus on topics covered in that term per GES curriculum.
12. Always supply a complete answer key: every fill_blank and theory question MUST have a model_answer, and every theory question MUST have a marking_guide breaking down how the marks are awarded. Never leave an answer blank.

DIAGRAM SPEC FORMAT (only when diagram_required = true).
Pick the MOST SPECIFIC "type" below and fill its fields EXACTLY so the figure can be drawn to scale.
Always include a short "caption". Supported types and their required fields:

- "bar_chart"        : data = { "Label A": number, "Label B": number, ... }
- "pie_chart"        : data = { "Slice A": number, "Slice B": number, ... }   (values are parts of a whole)
- "line_chart"       : points = { "x1": y1, "x2": y2, ... }, x_label, y_label
- "number_line"      : min, max, step, points = [ { "value": n, "label": "x" } ]
- "coordinate_plane" : x_range = [min,max], y_range = [min,max],
                       points = [ { "x": n, "y": n, "label": "A" } ],
                       lines  = [ { "from": [x,y], "to": [x,y], "label": "y=x" } ]
- "geometry"         : svg_hint = ONE of:
                         "triangle label=ABC" | "right_triangle a=3 b=4 c=5" |
                         "rect w=120 h=80 label=ABCD" | "circle r=60" |
                         "parallelogram w=120 h=70" | "trapezoid top=80 bottom=120 h=60" |
                         "rhombus d1=120 d2=80" | "pentagon n=5" | "hexagon n=6"
- "fraction"         : numerator, denominator
- "table"            : headers = ["Col 1","Col 2"], rows = [["a","b"], ...]
- "venn"             : set_a, set_b, only_a = [...], only_b = [...], both = [...]
- "timeline"         : events = [ { "year": "1957", "event": "Independence" } ]
- "flowchart"        : steps = ["Step 1","Step 2","Step 3"]   (also food chains / processes)
- "cycle"            : stages = ["Stage 1","Stage 2", ...]    (water cycle, life cycle, etc.)
- "tree"             : root, branches = [ { "label": "Branch", "children": ["Leaf 1","Leaf 2"] } ]
- "pyramid"          : levels = ["Top","Middle","Base"]

DIAGRAM RULES:
- CRITICAL: the diagram_spec MUST contain the ACTUAL DATA needed to answer the question — never a description alone. A bar_chart MUST include its data values; a pie/fraction MUST include the counts; "circle divided into 8 parts with 3 shaded" MUST be {"type":"pie_chart","data":{"Shaded":3,"Unshaded":5}} or a fraction with numerator/denominator. If you cannot provide the data, do NOT mark the question as needing a diagram.
- Numbers MUST be real numbers, not strings, and self-consistent (bar/pie values match their labels; a right_triangle satisfies a^2+b^2=c^2; pie slices are sensible parts of a whole).
- ALWAYS prefer a drawable structured type above. Only fall back to "biology"|"physics"|"map"|"science" (a labelled placeholder for hand-drawing) when the figure genuinely cannot be expressed with the structured types.
- Put the descriptive caption in "caption". Include a "type" field that exactly matches one of the names above.

OUTPUT FORMAT: Return ONLY a valid JSON array of question objects.
Each object must have these fields:
  type          : "mcq" | "fill_blank" | "theory" | "true_false" | "matching" | "comprehension"
  question      : string
  option_a      : string (MCQ only; empty string otherwise)
  option_b      : string (MCQ only; empty string otherwise)
  option_c      : string (MCQ only; empty string otherwise)
  option_d      : string (MCQ only; empty string otherwise)
  correct_answer: string (MCQ: "A","B","C", or "D"; true_false: "True" or "False"; fill_blank/theory: empty string)
                  For true_false, leave all options empty and put the statement in "question".
  model_answer  : string (REQUIRED — fill_blank: the exact accepted answer(s); theory: a concise but complete model answer; mcq: one short sentence explaining why the key is correct)
  marking_guide : array of strings (theory: the marking points and how the marks are awarded, one point per element; use [] for mcq and fill_blank)
  marks         : integer
  difficulty    : "easy" | "medium" | "hard"
  diagram_required: boolean
  diagram_type  : string (or empty)
  diagram_spec  : object | null
  extra_data    : object | null — REQUIRED for matching & comprehension (formats below), null otherwise

MATCHING extra_data FORMAT (put a short instruction such as "Match each item in Column A to its pair in Column B." in "question"):
{ "pairs": [ { "left": "Item", "right": "Correct match" }, ... ] }   (4-6 pairs)

COMPREHENSION extra_data FORMAT (put a short instruction such as "Read the passage and answer the questions that follow." in "question"):
{ "passage": "The full reading passage text.",
  "questions": [ { "text": "Sub-question?", "marks": 2, "model_answer": "Expected answer" }, ... ] }   (3-5 sub-questions)

Return ONLY the JSON array — no markdown fences, no explanation.
SYSTEM;

    // ── Build the per-batch user message ────────────────────────────────────
    // Counts are parameters so the same chunk can be regenerated in smaller
    // batches if one large request gets truncated at the token limit.
    $buildUser = static function (
        int $mcq, int $fill, int $theory, int $tf, int $match, int $comp, int $diag
    ) use (
        $subjectName, $subjectCode, $className, $term, $academicYear,
        $difficulty, $chunkTopic, $materialHeader, $chunkContent
    ): string {
        $totalQ = $mcq + $fill + $theory + $tf + $match + $comp;
        return <<<USER
SUBJECT: {$subjectName} ({$subjectCode})
CLASS LEVEL: {$className}
TERM: {$term}
ACADEMIC YEAR: {$academicYear}
DIFFICULTY: {$difficulty}
TOPIC / CHAPTER: {$chunkTopic}

GENERATE EXACTLY:
- {$mcq} Multiple Choice Questions (MCQ) — 4 options each, exactly one correct
- {$fill} Fill in the Blank questions (use _______ for the blank)
- {$theory} Theory / Short Answer questions
- {$tf} True/False questions (a clear statement that is definitively true or false)
- {$match} Matching questions (each provides 4-6 left items and their correct right-hand matches)
- {$comp} Comprehension questions (each provides ONE passage and 3-5 sub-questions on it)
- {$diag} of the above {$totalQ} questions MUST include a diagram spec

{$materialHeader}
{$chunkContent}
USER;
    };

    // ── One AI request for a given set of counts ────────────────────────────
    // RBCAIEngine (4-provider cascade) first, legacy cascade as fallback.
    // Returns ['questions' => array, 'engine' => string, 'truncated' => bool].
    $runBatch = function (
        int $mcq, int $fill, int $theory, int $tf, int $match, int $comp, int $diag
    ) use ($buildUser, $system): array {
        $user = $buildUser($mcq, $fill, $theory, $tf, $match, $comp, $diag);

        $rbc = RBCAIEngine::generate($user, $system, AI_QUESTION_MAX_TOKENS, false);
        if ($rbc['success']) {
            $raw    = (string) $rbc['content'];
            $engine = (string) $rbc['provider'];
        } else {
            $legacy = callAI($system, $user, AI_QUESTION_MAX_TOKENS);
            if ($legacy['error']) {
                throw new Exception($legacy['error']);
            }
            $raw    = (string) $legacy['result'];
            $engine = (string) $legacy['engine'];
        }

        // A response that is not cleanly-parseable JSON was almost certainly cut
        // off at the token limit. repairAndParseJSON still salvages the complete
        // objects, but we flag it so the caller can retry in smaller batches.
        $truncated = !jsonDecodesCleanly($raw);
        $questions = repairAndParseJSON($raw) ?? [];

        return ['questions' => $questions, 'engine' => $engine, 'truncated' => $truncated];
    };

    // ── Primary attempt: the whole chunk in one call ────────────────────────
    $primary    = $runBatch($mcqCount, $fillCount, $theoryCount, $tfCount, $matchingCount, $comprehensionCount, $diagramCount);
    $questions  = $primary['questions'];
    $engineUsed = $primary['engine'];

    // ── Fallback: split into smaller batches when the single call truncated ─
    $requestedTotal = $mcqCount + $fillCount + $theoryCount + $tfCount + $matchingCount + $comprehensionCount;
    if ($primary['truncated'] && $requestedTotal > AI_QUESTION_BATCH_SIZE) {
        error_log("[EduTrack AI] Chunk \"{$chunkTopic}\" truncated at {$requestedTotal} questions — retrying in smaller batches.");
        $batches  = splitQuestionCounts(
            $mcqCount, $fillCount, $theoryCount, $tfCount, $matchingCount, $comprehensionCount, $diagramCount,
            AI_QUESTION_BATCH_SIZE
        );
        $merged   = [];
        $batchEng = '';
        foreach ($batches as $b) {
            try {
                $r      = $runBatch($b['mcq'], $b['fill'], $b['theory'], $b['tf'], $b['match'], $b['comp'], $b['diag']);
                $merged = array_merge($merged, $r['questions']);
                if ($batchEng === '') {
                    $batchEng = $r['engine'];
                }
            } catch (Throwable $e) {
                error_log("[EduTrack AI] Batch retry failed for chunk \"{$chunkTopic}\": " . $e->getMessage());
            }
        }
        // Keep the batched result only if it recovered more than the single call.
        if (count($merged) > count($questions)) {
            $questions  = $merged;
            $engineUsed = $batchEng !== '' ? $batchEng : $engineUsed;
        }
    }

    if (!is_array($questions) || empty($questions)) {
        throw new Exception(
            'AI returned invalid or empty question data for chunk "' . $chunkTopic . '".'
        );
    }

    // ── INSERT questions ───────────────────────────────────────────────────
    // Ensure all dynamically-added columns exist BEFORE preparing the statement
    // (the prepare references chunk_topic and marking_guide).
    static $columnsEnsured = false;
    if (!$columnsEnsured) {
        $chk = $conn->query("SHOW COLUMNS FROM exam_questions LIKE 'chunk_topic'");
        if ($chk && $chk->num_rows === 0) {
            $conn->query("ALTER TABLE exam_questions
                ADD COLUMN chunk_topic VARCHAR(255) DEFAULT NULL,
                ADD COLUMN moderation_note TEXT DEFAULT NULL,
                ADD COLUMN refined TINYINT(1) NOT NULL DEFAULT 0");
        }
        $chk = $conn->query("SHOW COLUMNS FROM exam_questions LIKE 'marking_guide'");
        if ($chk && $chk->num_rows === 0) {
            $conn->query("ALTER TABLE exam_questions ADD COLUMN marking_guide TEXT DEFAULT NULL");
        }
        // Ensure extra_data column (matching pairs / comprehension passage + sub-questions).
        $chk = $conn->query("SHOW COLUMNS FROM exam_questions LIKE 'extra_data'");
        if ($chk && $chk->num_rows === 0) {
            $conn->query("ALTER TABLE exam_questions ADD COLUMN extra_data LONGTEXT DEFAULT NULL");
        }
        // Extend the question_type enum (runs the ALTER only once, when a value is missing).
        $col = $conn->query("SHOW COLUMNS FROM exam_questions LIKE 'question_type'");
        $colRow = $col ? $col->fetch_assoc() : null;
        if ($colRow && stripos((string) ($colRow['Type'] ?? ''), "'comprehension'") === false) {
            $conn->query("ALTER TABLE exam_questions MODIFY question_type ENUM('mcq','theory','fill_blank','true_false','matching','comprehension') NOT NULL DEFAULT 'mcq'");
        }
        $columnsEnsured = true;
    }

    $insertedIds = [];
    $stmt = $conn->prepare(
        "INSERT INTO exam_questions
          (subject_id, class_name, term, academic_year, question_type,
           question, option_a, option_b, option_c, option_d,
           correct_answer, model_answer, marking_guide, marks, difficulty,
           diagram_required, diagram_type, diagram_spec, extra_data,
           ai_generated, generation_job_id, created_by,
           chunk_topic)
         VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,1,?,?,?)"
    );

    foreach ($questions as $q) {
        $type          = (string) ($q['type']           ?? 'mcq');
        $question      = trim((string) ($q['question']  ?? ''));
        if ($question === '') {
            continue;
        }
        $optionA       = (string) ($q['option_a']       ?? '');
        $optionB       = (string) ($q['option_b']       ?? '');
        $optionC       = (string) ($q['option_c']       ?? '');
        $optionD       = (string) ($q['option_d']       ?? '');
        $correctAnswer = (string) ($q['correct_answer'] ?? '');
        $modelAnswer   = trim((string) ($q['model_answer'] ?? ''));
        $markingGuide  = '';
        if (!empty($q['marking_guide'])) {
            $markingGuide = is_array($q['marking_guide'])
                ? implode("\n", array_filter(array_map(static fn($p) => trim((string) $p), $q['marking_guide'])))
                : (string) $q['marking_guide'];
        }
        $marks         = max(1, (int) ($q['marks']      ?? 1));
        // Structural payload (matching pairs / comprehension passage + sub-questions).
        $extraData = (isset($q['extra_data']) && is_array($q['extra_data']))
            ? json_encode($q['extra_data'], JSON_UNESCAPED_UNICODE)
            : null;
        if ($type === 'comprehension' && isset($q['extra_data']['questions']) && is_array($q['extra_data']['questions'])) {
            $subMarks = array_sum(array_map(static fn($s) => max(1, (int) ($s['marks'] ?? 1)), $q['extra_data']['questions']));
            if ($subMarks > 0) { $marks = $subMarks; }
        } elseif ($type === 'matching' && isset($q['extra_data']['pairs']) && is_array($q['extra_data']['pairs'])) {
            $marks = max($marks, count($q['extra_data']['pairs']));
        }
        $qDifficulty   = (string) ($q['difficulty']     ?? $difficulty);
        $diagramReq    = !empty($q['diagram_required']) ? 1 : 0;
        $diagramType   = (string) ($q['diagram_type']   ?? '');
        $diagramSpec   = !empty($q['diagram_spec'])
            ? json_encode($q['diagram_spec'])
            : null;
        $topic         = $chunkTopic;

        $stmt->bind_param(
            'issssssssssssisisssiis',
            $subjectId,
            $className,
            $term,
            $academicYear,
            $type,
            $question,
            $optionA,
            $optionB,
            $optionC,
            $optionD,
            $correctAnswer,
            $modelAnswer,
            $markingGuide,
            $marks,
            $qDifficulty,
            $diagramReq,
            $diagramType,
            $diagramSpec,
            $extraData,
            $jobId,
            $userId,
            $topic
        );

        if ($stmt->execute()) {
            $insertedIds[] = (int) $conn->insert_id;
        }
    }
    $stmt->close();

    return [
        'inserted_ids' => $insertedIds,
        'questions'    => $questions,
        'engine'       => $engineUsed,
        'count'        => count($insertedIds),
        'attempts'     => [],
    ];
}

/**
 * Legacy wrapper — kept for backwards compatibility with any code that
 * still calls generateAndSaveQuestions() directly.
 * Treats the full content as a single chunk.
 */
function generateAndSaveQuestions(
    mysqli $conn,
    int    $subjectId,
    string $subjectCode,
    string $subjectName,
    string $className,
    string $term,
    string $academicYear,
    string $difficulty,
    int    $mcqCount,
    int    $theoryCount,
    int    $fillCount,
    int    $diagramCount,
    string $extractedContent,
    int    $jobId,
    int    $userId
): array {
    return generateChunkQuestions(
        $conn,
        $subjectId,
        $subjectCode,
        $subjectName,
        $className,
        $term,
        $academicYear,
        $difficulty,
        $mcqCount,
        $theoryCount,
        $fillCount,
        $diagramCount,
        $extractedContent,
        'Full Material',
        $jobId,
        $userId
    );
}
