<?php
/**
 * Class-name resolution for RBC AI actions.
 *
 * Every class-scoped action filters on an exact `students.class = ?`. When the
 * value comes from an LLM it is frequently a plausible-looking abbreviation —
 * "JH3" for "JHS 3", "basic6" for "Basic 6" — which matches zero rows. The
 * action then reports success with a count of zero and the operator is told
 * nothing went wrong.
 *
 * resolveClassName() maps loose input onto a real class, and returns a hard
 * error listing the valid classes when it cannot. Never guess silently.
 */

if (!defined('DB_HOST')) {
    require_once __DIR__ . '/../config.php';
}

if (!function_exists('rbcKnownClasses')) {
    /**
     * All class names known to the school, newest source first.
     * Primary: the Class Management table. Fallback: distinct student classes.
     *
     * @return string[]
     */
    function rbcKnownClasses(?mysqli $conn = null): array
    {
        static $cache = null;
        if ($cache !== null) return $cache;

        $conn = $conn ?: getDBConnection();
        $out  = [];

        if ($conn instanceof mysqli) {
            if ($r = @$conn->query("SELECT class_name FROM classes WHERE is_active = 1 ORDER BY class_name")) {
                while ($row = $r->fetch_assoc()) {
                    $name = trim((string) $row['class_name']);
                    if ($name !== '') $out[] = $name;
                }
            }
            if ($r = @$conn->query(
                "SELECT DISTINCT class FROM students
                 WHERE class IS NOT NULL AND class != '' AND status = 'active'
                 ORDER BY class"
            )) {
                while ($row = $r->fetch_assoc()) {
                    $name = trim((string) $row['class']);
                    if ($name !== '' && !in_array($name, $out, true)) $out[] = $name;
                }
            }
        }

        return $cache = $out;
    }
}

if (!function_exists('rbcNormaliseClassKey')) {
    /**
     * Reduce a class name to a comparison key: lowercase, alphanumerics only,
     * with common Ghanaian level words folded to a canonical stem.
     *
     * "JHS 3" / "jhs3" / "J.H.S. 3" / "JH3" / "js3"  -> "jhs3"
     * "Basic 6" / "basic6" / "B6"                    -> "basic6"
     */
    function rbcNormaliseClassKey(string $name): string
    {
        $k = strtolower(trim($name));
        $k = (string) preg_replace('/[^a-z0-9]+/', '', $k);

        // Longest patterns first so "jhs" is not partially eaten by "js".
        $stems = [
            '/^(juniorhighschool|juniorhigh|jhs|jss|jh|js)(\d+)$/' => 'jhs$2',
            '/^(seniorhighschool|seniorhigh|shs|sss|sh|ss)(\d+)$/' => 'shs$2',
            '/^(basic|bas|b)(\d+)$/'                               => 'basic$2',
            '/^(class|cls|c)(\d+)$/'                               => 'class$2',
            '/^(primary|pri|p)(\d+)$/'                             => 'primary$2',
            '/^(kindergarten|kg|k)(\d+)$/'                         => 'kg$2',
            '/^(nursery|nur|n)(\d+)$/'                             => 'nursery$2',
            '/^(creche|cr)(\d*)$/'                                 => 'creche$2',
        ];
        foreach ($stems as $re => $to) {
            if (preg_match($re, $k)) {
                return (string) preg_replace($re, $to, $k);
            }
        }
        return $k;
    }
}

if (!function_exists('resolveClassName')) {
    /**
     * Resolve loose input to a real class name.
     *
     * @param string      $input Raw class name (possibly from an LLM).
     * @param mysqli|null $conn
     *
     * @return array{ok: bool, class_name?: string, matched_by?: string, error?: string, valid_classes?: string[]}
     */
    function resolveClassName(string $input, ?mysqli $conn = null): array
    {
        $input = trim($input);
        $known = rbcKnownClasses($conn);

        if ($input === '') {
            return [
                'ok'            => false,
                'error'         => 'No class name was given.',
                'valid_classes' => $known,
            ];
        }
        if (!$known) {
            // No class list to check against — pass through rather than block.
            return ['ok' => true, 'class_name' => $input, 'matched_by' => 'unverified'];
        }

        // 1. Exact.
        foreach ($known as $c) {
            if ($c === $input) {
                return ['ok' => true, 'class_name' => $c, 'matched_by' => 'exact'];
            }
        }

        // 2. Case-insensitive / whitespace-insensitive.
        foreach ($known as $c) {
            if (strcasecmp(trim($c), $input) === 0) {
                return ['ok' => true, 'class_name' => $c, 'matched_by' => 'case'];
            }
        }

        // 3. Normalised stem — this is what catches "JH3" -> "JHS 3".
        $key = rbcNormaliseClassKey($input);
        $hits = [];
        foreach ($known as $c) {
            if (rbcNormaliseClassKey($c) === $key) $hits[] = $c;
        }
        if (count($hits) === 1) {
            return ['ok' => true, 'class_name' => $hits[0], 'matched_by' => 'normalised'];
        }
        if (count($hits) > 1) {
            return [
                'ok'            => false,
                'error'         => "\"{$input}\" is ambiguous — it could mean " . implode(' or ', $hits) . '.',
                'valid_classes' => $known,
            ];
        }

        // 4. Levenshtein, but only for a clearly-closest single candidate.
        $best = null; $bestScore = PHP_INT_MAX; $runnerUp = PHP_INT_MAX;
        foreach ($known as $c) {
            $d = levenshtein($key, rbcNormaliseClassKey($c));
            if ($d < $bestScore) { $runnerUp = $bestScore; $bestScore = $d; $best = $c; }
            elseif ($d < $runnerUp) { $runnerUp = $d; }
        }
        if ($best !== null && $bestScore <= 2 && $bestScore < $runnerUp) {
            return ['ok' => true, 'class_name' => $best, 'matched_by' => 'fuzzy'];
        }

        return [
            'ok'            => false,
            'error'         => "\"{$input}\" does not match any class. Valid classes are: " . implode(', ', $known) . '.',
            'valid_classes' => $known,
        ];
    }
}

if (!function_exists('rbcResolveClassParam')) {
    /**
     * Resolve $params['class_name'] in place.
     *
     * @return array{ok: bool, params?: array, error?: string, valid_classes?: string[]}
     *         ok=true with the class untouched when the field is absent/blank
     *         (callers that require it do their own emptiness check).
     */
    function rbcResolveClassParam(array $params, ?mysqli $conn = null, string $key = 'class_name'): array
    {
        $raw = trim((string) ($params[$key] ?? ''));
        if ($raw === '') {
            return ['ok' => true, 'params' => $params];
        }

        $res = resolveClassName($raw, $conn);
        if (!$res['ok']) {
            return ['ok' => false, 'error' => $res['error'], 'valid_classes' => $res['valid_classes'] ?? []];
        }

        $params[$key] = $res['class_name'];
        if (($res['matched_by'] ?? '') !== 'exact') {
            error_log("RBC class resolution: \"{$raw}\" -> \"{$res['class_name']}\" ({$res['matched_by']})");
        }
        return ['ok' => true, 'params' => $params, 'matched_by' => $res['matched_by'] ?? null];
    }
}
