<?php
/**
 * face.php – server-side face descriptor utilities.
 *
 * A descriptor is a 128-d float array (output of the face-api.js
 * face_recognition_model). We compare by Euclidean (L2) distance.
 *
 *   < 0.45   = very likely the same person
 *   0.45–0.55= probably the same; treat as match for OSCE purposes
 *   > 0.55   = different person; reject
 */

const FACE_MATCH_THRESHOLD = 0.50;
const FACE_DESCRIPTOR_LEN  = 128;

/** Validate that $arr is a 128-element float vector. */
function face_descriptor_is_valid($arr): bool
{
    if (!is_array($arr) || count($arr) !== FACE_DESCRIPTOR_LEN) return false;
    foreach ($arr as $v) {
        if (!is_numeric($v)) return false;
        $f = (float)$v;
        if ($f < -10 || $f > 10) return false; // sanity bounds
    }
    return true;
}

function face_descriptor_decode(string $json): ?array
{
    $d = json_decode($json, true);
    if (!face_descriptor_is_valid($d)) return null;
    // Normalize to float
    return array_map('floatval', $d);
}

function face_descriptor_encode(array $arr): string
{
    return json_encode(array_map('floatval', $arr), JSON_UNESCAPED_SLASHES);
}

/** Average multiple descriptors (used during enrollment for robustness). */
function face_descriptor_average(array $samples): ?array
{
    if (!$samples) return null;
    $sum = array_fill(0, FACE_DESCRIPTOR_LEN, 0.0);
    foreach ($samples as $s) {
        if (!face_descriptor_is_valid($s)) return null;
        for ($i = 0; $i < FACE_DESCRIPTOR_LEN; $i++) $sum[$i] += (float)$s[$i];
    }
    $n = count($samples);
    for ($i = 0; $i < FACE_DESCRIPTOR_LEN; $i++) $sum[$i] /= $n;
    return $sum;
}

/** Euclidean distance between two same-length numeric arrays. */
function face_distance(array $a, array $b): float
{
    $d = 0.0;
    $n = min(count($a), count($b));
    for ($i = 0; $i < $n; $i++) {
        $diff = (float)$a[$i] - (float)$b[$i];
        $d += $diff * $diff;
    }
    return sqrt($d);
}

/**
 * Scan all enrolled active users and return [user_row, distance] of the best match
 * (or null if no candidate is below threshold). Pure PHP – no DB-side ML required.
 */
function face_find_match(PDO $pdo, array $probe): ?array
{
    if (!face_descriptor_is_valid($probe)) return null;

    $stmt = $pdo->query("
        SELECT id, username, full_name, role, facility_id, active, face_descriptor
        FROM users
        WHERE active = 1
          AND face_login_enabled = 1
          AND face_descriptor IS NOT NULL
    ");
    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

    $best = null;
    foreach ($rows as $u) {
        $enrolled = face_descriptor_decode($u['face_descriptor']);
        if (!$enrolled) continue;
        $d = face_distance($probe, $enrolled);
        if ($best === null || $d < $best['distance']) {
            $best = ['user' => $u, 'distance' => $d];
        }
    }
    if (!$best) return null;
    if ($best['distance'] > FACE_MATCH_THRESHOLD) return null;
    return $best;
}
