<?php

/**
 * SBA Engine — School-Based Assessment (cumulative assessment).
 *
 * Single source of truth for:
 *   1. Level resolution         (class -> Lower Primary / Upper Primary / JHS / SHS)
 *   2. Weighting resolution      (class_subjects -> level setting -> 30/70 fallback)
 *   3. Default component sets    (fixed Ghanaian set when none defined)
 *   4. Component roll-up          (components -> raw continuous-assessment score)
 *   5. Grade computation          (from grade_boundaries)
 *   6. Persistence                (recompute + upsert the `scores` row)
 *
 * The roll-up writes into `scores.raw_class_score` / `total_score` / `grade`,
 * so the existing terminal reports reflect SBA with no report-side changes.
 */

if (!function_exists('getDBConnection')) {
    require_once dirname(__DIR__) . '/config.php';
}

/** Fixed Ghanaian fallback component set, used when a class-subject has none. */
const SBA_DEFAULT_COMPONENTS = [
    ['component_name' => 'Class Test',     'max_score' => 100, 'weight' => 1],
    ['component_name' => 'Project',        'max_score' => 100, 'weight' => 1],
    ['component_name' => 'Group Exercise', 'max_score' => 100, 'weight' => 1],
    ['component_name' => 'Homework',       'max_score' => 100, 'weight' => 1],
];

/**
 * Resolve a class to one of the four level buckets.
 * Uses classes.class_level when present, then heuristics on the class name.
 *
 * @return string one of: lower_primary | upper_primary | jhs | shs
 */
function sbaResolveLevel(mysqli $conn, string $class_name): string {
    $level_hint = '';
    $stmt = $conn->prepare("SELECT class_level FROM classes WHERE class_name = ? LIMIT 1");
    if ($stmt) {
        $stmt->bind_param('s', $class_name);
        $stmt->execute();
        $row = $stmt->get_result()->fetch_assoc();
        $stmt->close();
        $level_hint = (string) ($row['class_level'] ?? '');
    }

    $hay = strtolower($level_hint . ' ' . $class_name);

    // Explicit secondary / junior-high tokens take priority over numeric ranges.
    if (preg_match('/\b(shs|s\.h\.s|senior\s*high)\b/', $hay)) {
        return 'shs';
    }
    if (preg_match('/\b(jhs|j\.h\.s|junior\s*high)\b/', $hay)) {
        return 'jhs';
    }

    // Numeric "Basic N" / "Class N" / "Primary N" / "Grade N".
    if (preg_match('/\b(?:basic|class|primary|grade|p|b)\s*([1-9])\b/', $hay, $m)) {
        $n = (int) $m[1];
        if ($n >= 1 && $n <= 3) return 'lower_primary';
        if ($n >= 4 && $n <= 6) return 'upper_primary';
        if ($n >= 7 && $n <= 9) return 'jhs';
    }

    // Pre-school stages.
    if (preg_match('/\b(kg|k\.g|kindergarten|nursery|creche|pre)\b/', $hay)) {
        return 'lower_primary';
    }

    // Sensible default for unmatched names.
    return 'upper_primary';
}

/**
 * Read a level's configured weighting from settings ("ca/exam"), or null.
 *
 * @return array|null ['class_pct' => int, 'exam_pct' => int]
 */
function sbaLevelWeighting(string $level): ?array {
    $key = 'sba_weight_' . $level;
    if (!function_exists('getSettingValue')) {
        return null;
    }
    $raw = (string) (getSettingValue($key) ?? '');
    if (!preg_match('/^\s*(\d{1,3})\s*\/\s*(\d{1,3})\s*$/', $raw, $m)) {
        return null;
    }
    $ca = (int) $m[1];
    $ex = (int) $m[2];
    if ($ca + $ex !== 100) {
        return null;
    }
    return ['class_pct' => $ca, 'exam_pct' => $ex];
}

/**
 * Resolve the effective weighting for a class-subject.
 * Order: explicit class_subjects row -> level setting -> 30/70 fallback.
 *
 * @return array {
 *     class_pct:int, exam_pct:int, max_class:float, max_exam:float,
 *     source:string, level:string
 * }
 */
function sbaResolveWeighting(mysqli $conn, string $class_name, int $subject_id): array {
    $level = sbaResolveLevel($conn, $class_name);

    // Most specific: an admin-configured class_subjects row.
    $stmt = $conn->prepare(
        "SELECT class_score_percent, exam_score_percent, max_class_score, max_exam_score
           FROM class_subjects
          WHERE class_name = ? AND subject_id = ?
          LIMIT 1"
    );
    $cs = null;
    if ($stmt) {
        $stmt->bind_param('si', $class_name, $subject_id);
        $stmt->execute();
        $cs = $stmt->get_result()->fetch_assoc();
        $stmt->close();
    }

    if ($cs) {
        return [
            'class_pct' => (int) $cs['class_score_percent'],
            'exam_pct'  => (int) $cs['exam_score_percent'],
            'max_class' => (float) $cs['max_class_score'],
            'max_exam'  => (float) $cs['max_exam_score'],
            'source'    => 'class_subject',
            'level'     => $level,
        ];
    }

    // Next: a per-level setting.
    $lvl = sbaLevelWeighting($level);
    if ($lvl) {
        return [
            'class_pct' => $lvl['class_pct'],
            'exam_pct'  => $lvl['exam_pct'],
            'max_class' => 100.0,
            'max_exam'  => 100.0,
            'source'    => 'level',
            'level'     => $level,
        ];
    }

    // Hard fallback: Ghana basic-education default.
    return [
        'class_pct' => 30,
        'exam_pct'  => 70,
        'max_class' => 100.0,
        'max_exam'  => 100.0,
        'source'    => 'default_30_70',
        'level'     => $level,
    ];
}

/**
 * Return active component types for a class-subject in the given term/year.
 * If none exist, seed the fixed Ghanaian default set and return it.
 *
 * @return array<int,array> rows from sba_component_types
 */
function sbaEnsureComponents(
    mysqli $conn,
    string $class_name,
    int $subject_id,
    string $term,
    string $academic_year,
    ?int $teacher_id = null
): array {
    $components = sbaGetComponents($conn, $class_name, $subject_id, $term, $academic_year);
    if (!empty($components)) {
        return $components;
    }

    $ins = $conn->prepare(
        "INSERT INTO sba_component_types
            (class_name, subject_id, teacher_id, component_name, max_score, weight, display_order, term, academic_year, is_active)
         VALUES (?,?,?,?,?,?,?,?,?,1)"
    );
    $order = 0;
    foreach (SBA_DEFAULT_COMPONENTS as $c) {
        $name = $c['component_name'];
        $max  = (float) $c['max_score'];
        $wt   = (float) $c['weight'];
        $ins->bind_param(
            'siisddiss',
            $class_name, $subject_id, $teacher_id, $name, $max, $wt, $order, $term, $academic_year
        );
        $ins->execute();
        $order++;
    }
    $ins->close();

    return sbaGetComponents($conn, $class_name, $subject_id, $term, $academic_year);
}

/** Fetch active component types for a scope (no seeding). */
function sbaGetComponents(
    mysqli $conn,
    string $class_name,
    int $subject_id,
    string $term,
    string $academic_year
): array {
    $stmt = $conn->prepare(
        "SELECT id, component_name, max_score, weight, display_order
           FROM sba_component_types
          WHERE class_name = ? AND subject_id = ? AND term = ? AND academic_year = ? AND is_active = 1
          ORDER BY display_order, id"
    );
    $stmt->bind_param('siss', $class_name, $subject_id, $term, $academic_year);
    $stmt->execute();
    $rows = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
    $stmt->close();
    return $rows;
}

/**
 * Roll up a student's component scores into a continuous-assessment value
 * scaled to $max_class_score. Active components with no recorded score count
 * as zero. Returns null when there are no active components.
 */
function sbaRolledUpClassScore(
    mysqli $conn,
    int $student_id,
    array $components,
    float $max_class_score
): ?float {
    if (empty($components)) {
        return null;
    }

    // Load this student's scores keyed by component id.
    $ids = array_map(static fn($c) => (int) $c['id'], $components);
    $placeholders = implode(',', array_fill(0, count($ids), '?'));
    $types = str_repeat('i', count($ids) + 1);
    $sql = "SELECT component_type_id, score FROM sba_scores
             WHERE student_id = ? AND component_type_id IN ($placeholders)";
    $stmt = $conn->prepare($sql);
    $params = array_merge([$student_id], $ids);
    $stmt->bind_param($types, ...$params);
    $stmt->execute();
    $res = $stmt->get_result();
    $scores = [];
    while ($r = $res->fetch_assoc()) {
        $scores[(int) $r['component_type_id']] = (float) $r['score'];
    }
    $stmt->close();

    $weighted_fraction = 0.0;
    $total_weight = 0.0;
    foreach ($components as $c) {
        $max = (float) $c['max_score'];
        $wt  = (float) $c['weight'];
        if ($max <= 0 || $wt <= 0) {
            continue;
        }
        $raw = $scores[(int) $c['id']] ?? 0.0;
        $raw = max(0.0, min($raw, $max));
        $weighted_fraction += ($raw / $max) * $wt;
        $total_weight += $wt;
    }

    if ($total_weight <= 0) {
        return null;
    }

    return round(($weighted_fraction / $total_weight) * $max_class_score, 2);
}

/** Load grade boundaries for a class (falls back to 'default'). */
function sbaLoadGradeBoundaries(mysqli $conn, string $class_name, string $academic_year): array {
    $stmt = $conn->prepare(
        "SELECT grade, remark, min_score, max_score
           FROM grade_boundaries
          WHERE (class_name = ? OR class_name = 'default')
            AND (academic_year = ? OR academic_year IS NULL)
          ORDER BY min_score DESC"
    );
    $stmt->bind_param('ss', $class_name, $academic_year);
    $stmt->execute();
    $rows = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
    $stmt->close();
    return $rows;
}

/** Map a total score to its grade/remark using loaded boundaries. */
function sbaGradeFor(array $grade_bounds, float $total): array {
    foreach ($grade_bounds as $gb) {
        if ($total >= (float) $gb['min_score'] && $total <= (float) $gb['max_score']) {
            return ['grade' => $gb['grade'], 'remark' => $gb['remark']];
        }
    }
    return ['grade' => 'W', 'remark' => 'Weak'];
}

/**
 * Recompute and persist a single student's `scores` row, combining the
 * SBA-derived (or supplied) continuous-assessment score with the exam score.
 *
 * Pass overrides to set raw_class_score / raw_exam_score explicitly; otherwise
 * the engine derives CA from components (if any) and keeps the existing values.
 *
 * @param array $opts {
 *     class_name:string, subject_id:int, student_id:int, term:string,
 *     academic_year:string, components?:array,
 *     raw_class_score?:float, raw_exam_score?:float, grade_bounds?:array
 * }
 * @return array the persisted figures
 */
function sbaRecalcAndPersistScore(mysqli $conn, array $opts): array {
    $class_name = (string) $opts['class_name'];
    $subject_id = (int) $opts['subject_id'];
    $student_id = (int) $opts['student_id'];
    $term       = (string) $opts['term'];
    $ay         = (string) $opts['academic_year'];

    $w = sbaResolveWeighting($conn, $class_name, $subject_id);
    $max_cs = $w['max_class'] > 0 ? $w['max_class'] : 100.0;
    $max_es = $w['max_exam']  > 0 ? $w['max_exam']  : 100.0;

    // Existing row (for values not being changed this call).
    $stmt = $conn->prepare(
        "SELECT raw_class_score, raw_exam_score FROM scores
          WHERE student_id = ? AND subject_id = ? AND term = ? AND academic_year = ? LIMIT 1"
    );
    $stmt->bind_param('isss', $student_id, $subject_id, $term, $ay);
    $stmt->execute();
    $existing = $stmt->get_result()->fetch_assoc();
    $stmt->close();

    // Continuous-assessment score: explicit override -> component roll-up -> existing.
    $components = $opts['components'] ?? sbaGetComponents($conn, $class_name, $subject_id, $term, $ay);
    if (array_key_exists('raw_class_score', $opts)) {
        $raw_cs = max(0.0, min((float) $opts['raw_class_score'], $max_cs));
    } else {
        $rolled = sbaRolledUpClassScore($conn, $student_id, $components, $max_cs);
        $raw_cs = $rolled !== null ? $rolled : (float) ($existing['raw_class_score'] ?? 0);
    }

    // Exam score: explicit override -> existing.
    $raw_es = array_key_exists('raw_exam_score', $opts)
        ? max(0.0, min((float) $opts['raw_exam_score'], $max_es))
        : (float) ($existing['raw_exam_score'] ?? 0);

    $w_cs  = $max_cs > 0 ? ($raw_cs / $max_cs) * $w['class_pct'] : 0;
    $w_es  = $max_es > 0 ? ($raw_es / $max_es) * $w['exam_pct']  : 0;
    $total = round($w_cs + $w_es, 2);

    $bounds = $opts['grade_bounds'] ?? sbaLoadGradeBoundaries($conn, $class_name, $ay);
    $gr = sbaGradeFor($bounds, $total);

    $up = $conn->prepare(
        "INSERT INTO scores
            (student_id, subject_id, class_name, class_score, exam_score,
             raw_class_score, raw_exam_score, weighted_class_score, weighted_exam_score,
             total_score, grade, remark, term, academic_year)
         VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
         ON DUPLICATE KEY UPDATE
            class_name           = VALUES(class_name),
            class_score          = VALUES(class_score),
            exam_score           = VALUES(exam_score),
            raw_class_score      = VALUES(raw_class_score),
            raw_exam_score       = VALUES(raw_exam_score),
            weighted_class_score = VALUES(weighted_class_score),
            weighted_exam_score  = VALUES(weighted_exam_score),
            total_score          = VALUES(total_score),
            grade                = VALUES(grade),
            remark               = VALUES(remark),
            updated_at           = NOW()"
    );
    $grade = $gr['grade'];
    $remark = $gr['remark'];
    $up->bind_param(
        'iisdddddddssss',
        $student_id, $subject_id, $class_name,
        $raw_cs, $raw_es,
        $raw_cs, $raw_es,
        $w_cs, $w_es,
        $total, $grade, $remark,
        $term, $ay
    );
    $up->execute();
    $up->close();

    return [
        'raw_class_score' => $raw_cs,
        'raw_exam_score'  => $raw_es,
        'total_score'     => $total,
        'grade'           => $grade,
        'remark'          => $remark,
        'weighting'       => $w,
    ];
}
