<?php
/**
 * lib/helpers.php — Utilidades compartidas: JSON I/O, escape HTML, CSRF y generadores.
 */

declare(strict_types=1);

/* ------------------------------------------------------------------ *
 *  Salida / entrada JSON (API)
 * ------------------------------------------------------------------ */

/**
 * Emite una respuesta JSON y termina la ejecución.
 *
 * @param array<string,mixed> $arr  Payload a serializar.
 * @param int                 $code Código HTTP.
 */
function json_out(array $arr, int $code = 200): never
{
    http_response_code($code);
    header('Content-Type: application/json; charset=utf-8');
    header('X-Content-Type-Options: nosniff');
    echo json_encode($arr, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    exit;
}

/**
 * Lee y decodifica el cuerpo JSON de la petición.
 * Devuelve un array (vacío si el cuerpo no es JSON válido).
 *
 * @return array<string,mixed>
 */
function read_json_body(): array
{
    $raw = file_get_contents('php://input');
    if ($raw === false || $raw === '') {
        return [];
    }
    $data = json_decode($raw, true);
    return is_array($data) ? $data : [];
}

/* ------------------------------------------------------------------ *
 *  Escape HTML (panel)
 * ------------------------------------------------------------------ */

/**
 * Escapa una cadena para salida HTML segura (anti-XSS).
 */
function e(?string $value): string
{
    return htmlspecialchars($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

/* ------------------------------------------------------------------ *
 *  CSRF (formularios del panel)
 * ------------------------------------------------------------------ */

/**
 * Devuelve (creándolo si hace falta) el token CSRF de la sesión.
 */
function csrf_token(): string
{
    if (session_status() !== PHP_SESSION_ACTIVE) {
        session_start();
    }
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

/**
 * Campo oculto listo para insertar en un formulario.
 */
function csrf_field(): string
{
    return '<input type="hidden" name="csrf_token" value="' . e(csrf_token()) . '">';
}

/**
 * Verifica el token CSRF recibido por POST. Aborta con 400 si no coincide.
 */
function csrf_check(): void
{
    if (session_status() !== PHP_SESSION_ACTIVE) {
        session_start();
    }
    $sent = $_POST['csrf_token'] ?? '';
    $real = $_SESSION['csrf_token'] ?? '';
    if (!is_string($sent) || $real === '' || !hash_equals($real, $sent)) {
        http_response_code(400);
        exit('CSRF token inválido. Recarga la página e inténtalo de nuevo.');
    }
}

/* ------------------------------------------------------------------ *
 *  Generadores
 * ------------------------------------------------------------------ */

/**
 * Genera un device_key con formato "IBO-XXXX-XXXX" (alfanumérico sin ambiguos).
 */
function generate_device_key(): string
{
    return 'IBO-' . random_token(4) . '-' . random_token(4);
}

/**
 * Genera un código de activación con formato "XXXX-XXXX".
 */
function generate_activation_code(): string
{
    return random_token(4) . '-' . random_token(4);
}

/**
 * Genera un código de activación NUMÉRICO (solo dígitos, sin letras ni mayúsculas).
 * Pensado para tipearlo con el mando en la app. Por defecto 6 dígitos.
 */
function generate_numeric_code(int $len = 6): string
{
    $out = '';
    for ($i = 0; $i < $len; $i++) {
        $out .= (string) random_int(0, 9);
    }
    return $out;
}

/**
 * Bloque aleatorio de $len caracteres del alfabeto sin caracteres ambiguos
 * (sin 0/O ni 1/I) para que sea legible en pantalla/QR.
 */
function random_token(int $len): string
{
    $alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
    $max = strlen($alphabet) - 1;
    $out = '';
    for ($i = 0; $i < $len; $i++) {
        $out .= $alphabet[random_int(0, $max)];
    }
    return $out;
}

/* ------------------------------------------------------------------ *
 *  Validación / formato
 * ------------------------------------------------------------------ */

/**
 * Normaliza una MAC: mayúsculas y separadores ":" . Devuelve null si no es válida.
 * Acepta entrada con ":", "-" o sin separadores.
 */
function normalize_mac(?string $mac): ?string
{
    if ($mac === null) {
        return null;
    }
    $hex = strtoupper(preg_replace('/[^0-9A-Fa-f]/', '', $mac) ?? '');
    if (strlen($hex) !== 12) {
        return null;
    }
    return implode(':', str_split($hex, 2));
}

/**
 * Convierte un valor cualquiera a int 0/1 (para checkboxes y flags).
 */
function to_bool_int(mixed $value): int
{
    if (is_string($value)) {
        $value = strtolower(trim($value));
        return in_array($value, ['1', 'true', 'on', 'yes'], true) ? 1 : 0;
    }
    return $value ? 1 : 0;
}

/**
 * Valida que una fecha tenga formato YYYY-MM-DD (o esté vacía).
 * Devuelve la fecha normalizada o null.
 */
function valid_date(?string $date): ?string
{
    if ($date === null || trim($date) === '') {
        return null;
    }
    $date = trim($date);
    $dt = DateTime::createFromFormat('Y-m-d', $date);
    if ($dt && $dt->format('Y-m-d') === $date) {
        return $date;
    }
    return null;
}

/**
 * Valida un color hexadecimal (#RGB o #RRGGBB). Si no es válido, devuelve $default.
 */
function valid_color(?string $color, string $default = '#000000'): string
{
    $color = trim((string) $color);
    if (preg_match('/^#(?:[0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/', $color)) {
        return strtolower($color);
    }
    return $default;
}

/**
 * Marca de tiempo actual en formato ISO (UTC), para last_seen/created_at.
 */
function now_iso(): string
{
    return gmdate('Y-m-d H:i:s');
}
