<?php
/**
 * TechShop Perú - Funciones Globales
 * 
 * Utilidades generales usadas en toda la aplicación.
 * 
 * @package TechShop Perú
 * @version 1.0
 */

if (!defined('SECURE_ACCESS')) {
    die('Acceso directo no permitido');
}

// ============================================
// FUNCIONES DE SANITIZACIÓN Y SEGURIDAD
// ============================================

/**
 * Escapa una cadena para mostrarla en HTML de forma segura.
 */
function e(?string $value): string
{
    return htmlspecialchars((string) ($value ?? ''), ENT_QUOTES | ENT_HTML5, 'UTF-8');
}

/**
 * Sanitiza una cadena de entrada (quita espacios, strips tags).
 * Acepta strings, números y arreglos simples.
 */
function sanitize($value): string
{
    if (is_array($value)) {
        $value = array_map(static function ($item): string {
            return sanitize($item);
        }, $value);
        return trim(strip_tags(implode(', ', $value)));
    }

    if ($value === null) {
        return '';
    }

    $stringValue = is_scalar($value) ? (string) $value : '';
    return trim(strip_tags($stringValue));
}

/**
 * Valida y guarda de forma segura una imagen subida (unitaria).
 *
 * Centraliza la lógica usada en checkout (voucher), panel (imagen de producto)
 * y API (foto de reseña): extensión permitida → tamaño máximo → getimagesize()
 * (rechaza archivos que no son imágenes reales) → nombre aleatorio → move_uploaded_file.
 *
 * @param array  $file        Fila de $_FILES (p. ej. $_FILES['imagen'])
 * @param string $subfolder   Subcarpeta dentro de uploads/ (p. ej. 'products')
 * @param string $prefix      Prefijo del nombre del archivo (p. ej. 'prod')
 * @param int    $maxBytes    Tamaño máximo en bytes
 * @param array  $allowedExts Extensiones permitidas (sin punto, minúsculas)
 * @param string $etiqueta    Nombre del tipo de archivo para mensajes (p. ej. 'voucher', 'foto')
 * @return array{ok: bool, path: ?string, error: string} path = nombre del archivo guardado
 */
function save_uploaded_image(array $file, string $subfolder, string $prefix = 'img', int $maxBytes = 3 * 1024 * 1024, array $allowedExts = ['jpg', 'jpeg', 'png', 'webp'], string $etiqueta = 'imagen'): array
{
    if (empty($file['name'])) {
        return ['ok' => true, 'path' => null, 'error' => ''];
    }

    if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
        return ['ok' => false, 'path' => null, 'error' => 'No se pudo subir el ' . $etiqueta . ' (¿supera el límite del servidor?).'];
    }

    $ext = strtolower(pathinfo((string) $file['name'], PATHINFO_EXTENSION));
    if (!in_array($ext, $allowedExts, true)) {
        return ['ok' => false, 'path' => null, 'error' => 'El ' . $etiqueta . ' debe ser una imagen: ' . implode(', ', array_map('strtoupper', $allowedExts)) . '.'];
    }

    if ((int) ($file['size'] ?? 0) > $maxBytes) {
        return ['ok' => false, 'path' => null, 'error' => 'El ' . $etiqueta . ' supera el máximo de ' . (int) ($maxBytes / 1024 / 1024) . 'MB.'];
    }

    // Rechazar archivos que no sean imágenes reales (basura renombrada a .jpg/.png).
    if (@getimagesize((string) $file['tmp_name']) === false) {
        return ['ok' => false, 'path' => null, 'error' => 'El archivo no es una imagen válida. Sube una foto real del ' . $etiqueta . '.'];
    }

    $destDir = UPLOADS_PATH . trim($subfolder, '/\\') . DIRECTORY_SEPARATOR;
    if (!is_dir($destDir) && !@mkdir($destDir, 0755, true) && !is_dir($destDir)) {
        return ['ok' => false, 'path' => null, 'error' => 'No se pudo crear la carpeta de destino en uploads/.'];
    }

    $filename = $prefix . '_' . time() . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
    if (!move_uploaded_file((string) $file['tmp_name'], $destDir . $filename)) {
        return ['ok' => false, 'path' => null, 'error' => 'No se pudo guardar el ' . $etiqueta . '. Verifica los permisos de escritura de uploads/.'];
    }

    return ['ok' => true, 'path' => $filename, 'error' => ''];
}

/**
 * Convierte contenido Markdown simple a HTML.
 * Ideal para páginas de contenido y descripciones.
 */
function render_markdown(string $content): string
{
    $text = trim($content);
    if ($text === '') {
        return '';
    }

    $lines = preg_split('/\r\n|\r|\n/', $text);
    $html = [];
    $inList = false;

    foreach ($lines as $line) {
        $line = rtrim($line);

        if (preg_match('/^#{1,6}\s+(.*)$/', $line, $m)) {
            if ($inList) {
                $html[] = '</ul>';
                $inList = false;
            }
            $level = strlen($m[0]) - strlen(ltrim($m[0], '#'));
            $html[] = '<h' . min($level, 6) . '>' . e(trim($m[1])) . '</h' . min($level, 6) . '>';
            continue;
        }

        if (preg_match('/^[-*]\s+(.*)$/', $line, $m)) {
            if (!$inList) {
                $html[] = '<ul>';
                $inList = true;
            }
            $html[] = '<li>' . e(trim($m[1])) . '</li>';
            continue;
        }

        if ($inList) {
            $html[] = '</ul>';
            $inList = false;
        }

        if ($line === '') {
            continue;
        }

        $line = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $line);
        $line = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $line);
        $line = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/', '<a href="$2">$1</a>', $line);
        $html[] = '<p>' . $line . '</p>';
    }

    if ($inList) {
        $html[] = '</ul>';
    }

    return implode("\n", $html);
}

/**
 * Renderiza contenido seguro mezclando HTML y Markdown.
 * Usa HTML cuando ya viene preparado; convierte markdown simple si no.
 */
function render_content(string $content): string
{
    $text = trim($content);
    if ($text === '') {
        return '';
    }

    $looksLikeMarkdown = str_contains($text, '#') || str_contains($text, '##') || str_contains($text, '- ') || str_contains($text, '**') || str_contains($text, '[');
    if ($looksLikeMarkdown) {
        return render_markdown($text);
    }

    return $text;
}

/**
 * Asegura que la sesión esté iniciada antes de usarla.
 */
function ensure_session_started(): void
{
    if (session_status() === PHP_SESSION_NONE) {
        session_start();
    }
}

/**
 * Devuelve el nombre de tabla de configuración compatible con la DB actual.
 */
function config_table_name(): string
{
    static $table = null;
    if ($table !== null) {
        return $table;
    }

    try {
        $db = db();
        $stmt = $db->query("SHOW TABLES LIKE 'configuracion'");
        if ($stmt && $stmt->fetch()) {
            $table = 'configuracion';
        } else {
            $stmt = $db->query("SHOW TABLES LIKE 'configuraciones'");
            if ($stmt && $stmt->fetch()) {
                $table = 'configuraciones';
            } else {
                $table = 'configuracion';
            }
        }
    } catch (Throwable $e) {
        $table = 'configuracion';
    }

    return $table;
}

/**
 * Carga las configuraciones desde la tabla compatible.
 */
function load_db_config(): array
{
    try {
        $table = config_table_name();
        $stmt = db()->query("SELECT clave, valor FROM `{$table}`");
        $raw = $stmt ? $stmt->fetchAll(PDO::FETCH_KEY_PAIR) : [];

        // Normaliza claves legadas (dump original) a las canónicas que usa el
        // panel y el código (TIENDA_NOMBRE, TIENDA_EMAIL, IGV_PORCENTAJE, ...).
        // Evita que una instalación fresca quede con campos vacíos en el panel
        // y que la tienda use los fallbacks en vez de los valores reales de la BD.
        $legacyMap = [
            'nombre_tienda'  => 'tienda_nombre',
            'slogan'         => 'tienda_slogan',
            'email_contacto' => 'tienda_email',
            'telefono'       => 'tienda_telefono',
            'whatsapp'       => 'tienda_whatsapp',
            'direccion'      => 'tienda_direccion',
            'ruc'            => 'tienda_ruc',
            'igv'            => 'igv_porcentaje',
        ];

        // Preferir la clave canónica: si ya existe una fila canónica, la legada
        // no debe pisarla (evita resultados no deterministas con filas mixtas).
        $hasCanonica = [];
        foreach ($raw as $clave => $valor) {
            $canonica = $legacyMap[$clave] ?? $clave;
            if ($canonica === $clave) {
                $hasCanonica[$canonica] = true;
            }
        }

        $normalized = [];
        foreach ($raw as $clave => $valor) {
            $canonica = $legacyMap[$clave] ?? $clave;
            if ($canonica !== $clave && isset($hasCanonica[$canonica])) {
                continue; // existe la canónica real; ignorar la legada
            }
            if (!array_key_exists($canonica, $normalized)) {
                $normalized[$canonica] = $valor;
            }
        }
        return $normalized;
    } catch (Throwable $e) {
        return [];
    }
}

/**
 * Guarda una configuración en la tabla compatible.
 */
function save_db_config(string $clave, string $valor): bool
{
    try {
        $table = config_table_name();
        db()->prepare("INSERT INTO `{$table}` (clave, valor) VALUES (?, ?) ON DUPLICATE KEY UPDATE valor = VALUES(valor)")
            ->execute([$clave, $valor]);
        return true;
    } catch (Throwable $e) {
        return false;
    }
}

/**
 * Genera un token CSRF y lo guarda en sesión.
 */
function csrf_token(): string
{
    ensure_session_started();

    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

/**
 * Campo oculto HTML para formularios CSRF.
 */
function csrf_field(): string
{
    return '<input type="hidden" name="csrf_token" value="' . csrf_token() . '">';
}

/**
 * Verifica un token CSRF enviado por POST.
 */
function csrf_verify(): bool
{
    ensure_session_started();

    if (!isset($_POST['csrf_token']) || !isset($_SESSION['csrf_token'])) {
        return false;
    }

    return hash_equals($_SESSION['csrf_token'], (string) $_POST['csrf_token']);
}

/**
 * Regenera el token CSRF después de usarlo.
 */
function csrf_regenerate(): void
{
    unset($_SESSION['csrf_token']);
}

/**
 * Verifica un token CSRF enviado por GET (acciones tipo ?accion=X&csrf=TOKEN).
 * Compara con hash_equals para evitar ataques de timing.
 */
function csrf_verify_get(): bool
{
    ensure_session_started();

    if (!isset($_GET['csrf']) || !isset($_SESSION['csrf_token'])) {
        return false;
    }

    return hash_equals($_SESSION['csrf_token'], (string) $_GET['csrf']);
}

// ============================================
// FUNCIONES DE FORMATEO
// ============================================

/**
 * Formatea un número como precio en soles.
 */
function format_precio(float $precio): string
{
    $moneda = defined('MONEDA') && MONEDA !== '' ? MONEDA : 'S/';
    return $moneda . ' ' . number_format($precio, 2, '.', ',');
}

/**
 * Calcula el porcentaje de descuento.
 */
function calc_descuento(float $precio, float $precio_oferta): int
{
    if ($precio <= 0) return 0;
    return (int) round((($precio - $precio_oferta) / $precio) * 100);
}

/**
 * Determina si un producto tiene oferta válida (precio_oferta menor al precio).
 *
 * Acepta un array con claves 'precio' y 'precio_oferta' (como los registros de
 * la tabla productos) o dos números.
 */
function tiene_oferta(float|array $precio, float $precio_oferta = 0): bool
{
    if (is_array($precio)) {
        $precio_oferta = (float) ($precio['precio_oferta'] ?? 0);
        $precio = (float) ($precio['precio'] ?? 0);
    }
    return $precio_oferta > 0 && $precio_oferta < $precio;
}

/**
 * Devuelve el precio final a pagar por un producto (oferta si es válida).
 *
 * Acepta un array con claves 'precio' y 'precio_oferta' (como los registros de
 * la tabla productos) o dos números.
 */
function precio_final(float|array $precio, float $precio_oferta = 0): float
{
    if (is_array($precio)) {
        $precio_oferta = (float) ($precio['precio_oferta'] ?? 0);
        $precio = (float) ($precio['precio'] ?? 0);
    }
    return tiene_oferta($precio, $precio_oferta) ? $precio_oferta : $precio;
}

/**
 * Formatea fecha en español.
 */
function format_fecha(string $fecha, string $formato = 'd/m/Y'): string
{
    if ($fecha === '') {
        return '';
    }

    try {
        $dt = new DateTime($fecha);
        return $dt->format($formato);
    } catch (Throwable $e) {
        return $fecha;
    }
}

// ============================================
// FUNCIONES DE URL Y RUTAS
// ============================================

/**
 * Genera una URL completa con BASE_URL.
 */
function url(string $path = ''): string
{
    if ($path === '') {
        return BASE_URL;
    }

    if (preg_match('#^(https?:)?//#i', $path) || preg_match('#^[a-z][a-z0-9+.-]*:#i', $path)) {
        return $path;
    }

    $cleanPath = ltrim($path, '/');
    return $cleanPath === '' ? BASE_URL : rtrim(BASE_URL, '/') . '/' . $cleanPath;
}

/**
 * Redirecciona a una ruta relativa.
 */
function redirect(string $path = ''): void
{
    $url = url($path);

    if (headers_sent()) {
        echo '<script>window.location.replace(' . json_encode($url, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) . ');</script>';
    } else {
        header('Location: ' . $url, true, 302);
    }

    exit;
}

/**
 * Verifica si la página actual coincide con la ruta dada.
 */
function is_active(string $path): string
{
    $path = trim($path, '/');
    $current = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?: '';
    $basePath = parse_url(BASE_URL, PHP_URL_PATH) ?: '';

    if ($basePath !== '') {
        $current = preg_replace('#^' . preg_quote($basePath, '#') . '#', '', $current);
    }

    $current = trim($current, '/');
    if ($current === 'index.php') {
        $current = '';
    }

    if ($path === '' && $current === '') {
        return 'active';
    }

    if ($current === $path || ($path !== '' && str_starts_with($current, $path))) {
        return 'active';
    }

    return '';
}

// ============================================
// FUNCIONES DE AUTENTICACIÓN
// ============================================

/**
 * Verifica si el usuario ha iniciado sesión.
 */
function is_logged_in(): bool
{
    ensure_session_started();
    return isset($_SESSION['usuario_id']);
}

/**
 * Verifica si el usuario es admin.
 */
function is_admin(): bool
{
    return is_logged_in() && ($_SESSION['usuario_rol'] ?? '') === 'admin';
}

/**
 * Requiere autenticación, redirige al login si no.
 */
function require_login(): void
{
    if (!is_logged_in()) {
        $_SESSION['redirect_after_login'] = $_SERVER['REQUEST_URI'];
        $_SESSION['mensaje'] = ['tipo' => 'warning', 'texto' => 'Debes iniciar sesión para acceder a esta sección.'];
        if (str_contains($_SERVER['REQUEST_URI'] ?? '', 'panel-admin-tech')) {
            redirect('panel-admin-tech/login.php');
        } else {
            redirect('pages/login.php');
        }
    }
}

/**
 * Requiere rol admin.
 */
function require_admin(): void
{
    if (!is_logged_in()) {
        $_SESSION['redirect_after_login'] = $_SERVER['REQUEST_URI'];
        redirect('panel-admin-tech/login.php');
    }
    if (!is_admin()) {
        $_SESSION['mensaje'] = ['tipo' => 'error', 'texto' => 'Acceso denegado. Se requieren permisos de administrador.'];
        redirect('panel-admin-tech/login.php');
    }
}

// ============================================
// PROTECCIÓN ANTI-BRUTE-FORCE (LOGIN)
// ============================================

/**
 * Límite de intentos fallidos antes de bloquear el login.
 */
define('LOGIN_MAX_ATTEMPTS', 5);

/**
 * Duración del bloqueo en segundos (15 minutos).
 */
define('LOGIN_LOCKOUT_SECONDS', 900);

/**
 * Clave de intentos: combina email normalizado + IP para evitar
 * que un atacante rote cuentas o IPs trivialmente.
 */
function login_attempts_key(string $email): string
{
    $ip = $_SERVER['REMOTE_ADDR'] ?? 'desconocida';
    return hash('sha256', strtolower(trim($email)) . '|' . $ip);
}

/**
 * Lee los intentos fallidos registrados para una credencial.
 */
function login_attempts_get(string $email): array
{
    ensure_session_started();
    $key = login_attempts_key($email);
    return $_SESSION['login_attempts'][$key] ?? ['count' => 0, 'locked_until' => 0];
}

/**
 * Indica si el login está temporalmente bloqueado para esa credencial
 * y devuelve los segundos restantes (0 = no bloqueado).
 */
function login_attempts_remaining(string $email): int
{
    $data = login_attempts_get($email);
    $lockedUntil = (int) ($data['locked_until'] ?? 0);
    return max(0, $lockedUntil - time());
}

/**
 * True si el login está bloqueado.
 */
function login_is_locked(string $email): bool
{
    return login_attempts_remaining($email) > 0;
}

/**
 * Registra un intento fallido. Al superar el límite, activa el bloqueo.
 */
function login_attempts_failed(string $email): void
{
    ensure_session_started();
    $key = login_attempts_key($email);
    $data = login_attempts_get($email);

    // Si el bloqueo anterior ya expiró, reiniciar el contador
    if ($data['count'] >= LOGIN_MAX_ATTEMPTS && $data['locked_until'] <= time()) {
        $data = ['count' => 0, 'locked_until' => 0];
    }

    $data['count']++;
    if ($data['count'] >= LOGIN_MAX_ATTEMPTS) {
        $data['locked_until'] = time() + LOGIN_LOCKOUT_SECONDS;
    }

    $_SESSION['login_attempts'][$key] = $data;
}

/**
 * Limpia los intentos fallidos tras un login exitoso.
 */
function login_attempts_reset(string $email): void
{
    ensure_session_started();
    unset($_SESSION['login_attempts'][login_attempts_key($email)]);
}

/**
 * Devuelve cuántos intentos quedan antes del bloqueo.
 */
function login_attempts_left(string $email): int
{
    $data = login_attempts_get($email);
    if (login_is_locked($email)) {
        return 0;
    }
    return max(0, LOGIN_MAX_ATTEMPTS - (int) ($data['count'] ?? 0));
}

// ============================================
// FUNCIONES DE MENSAJES FLASH
// ============================================

/**
 * Obtiene y limpia un mensaje flash.
 */
function flash_message(): string
{
    ensure_session_started();

    if (!isset($_SESSION['mensaje'])) {
        return '';
    }
    $msg = $_SESSION['mensaje'];
    unset($_SESSION['mensaje']);

    $tipo = $msg['tipo'] ?? 'info';
    $iconos = [
        'success' => 'bi-check-circle-fill',
        'error'   => 'bi-exclamation-triangle-fill',
        'warning' => 'bi-exclamation-circle-fill',
        'info'    => 'bi-info-circle-fill',
    ];
    $icono = $iconos[$tipo] ?? 'bi-info-circle-fill';

    return sprintf(
        '<div class="alert alert-%s alert-dismissible fade show d-flex align-items-center" role="alert">
            <i class="bi %s me-2"></i>
            <span>%s</span>
            <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Cerrar"></button>
        </div>',
        $tipo === 'error' ? 'danger' : $tipo,
        $icono,
        e($msg['texto'])
    );
}

/**
 * Establece un mensaje flash.
 */
function set_flash(string $tipo, string $texto): void
{
    ensure_session_started();
    $_SESSION['mensaje'] = ['tipo' => $tipo, 'texto' => $texto];
}

// ============================================
// FUNCIONES DE PRODUCTOS
// ============================================

/**
 * Obtiene productos del catálogo con sus categorías y marcas.
 *
 * Reemplaza los bloques duplicados de index.php (destacados, ofertas, nuevos).
 *
 * @param string $tipo  'destacados' | 'ofertas' | 'nuevos'
 * @param int    $limite Cantidad máxima de productos
 * @return array Lista de productos (vacía si hay error o tipo inválido)
 */
function get_productos_home(string $tipo, int $limite = 8): array
{
    $config = [
        // tipo => [filtro SQL, orden SQL]
        'destacados' => ['p.destacado = 1', 'p.fecha_creacion DESC'],
        'ofertas'    => ['p.en_oferta = 1', 'p.calificacion DESC'],
        'nuevos'     => ['1 = 1',            'p.fecha_creacion DESC'],
    ];

    if (!isset($config[$tipo])) {
        return [];
    }

    [$filtro, $orden] = $config[$tipo];

    try {
        $stmt = db()->prepare('
            SELECT p.*, c.nombre AS categoria_nombre, c.slug AS categoria_slug,
                   m.nombre AS marca_nombre, m.slug AS marca_slug
            FROM productos p
            JOIN categorias c ON p.categoria_id = c.id
            JOIN marcas m ON p.marca_id = m.id
            WHERE p.activo = 1 AND ' . $filtro . '
            ORDER BY ' . $orden . '
            LIMIT ' . max(1, (int) $limite)
        );
        $stmt->execute();
        return $stmt->fetchAll();
    } catch (PDOException $e) {
        return [];
    }
}

/**
 * Obtiene estrellas HTML según calificación.
 */
function render_estrellas(float $calificacion): string
{
    $html = '<div class="star-rating">';
    for ($i = 1; $i <= 5; $i++) {
        if ($i <= floor($calificacion)) {
            $html .= '<i class="bi bi-star-fill text-warning"></i>';
        } elseif ($i - 0.5 <= $calificacion) {
            $html .= '<i class="bi bi-star-half text-warning"></i>';
        } else {
            $html .= '<i class="bi bi-star text-warning"></i>';
        }
    }
    $html .= '</div>';
    return $html;
}

// ============================================
// FUNCIONES DE CARRITO
// ============================================

/**
 * Cuenta los ítems en el carrito del usuario/sesión.
 */
function cart_count(): int
{
    try {
        $db = db();
        if (is_logged_in()) {
            $stmt = $db->prepare('SELECT SUM(cantidad) FROM carrito WHERE usuario_id = ?');
            $stmt->execute([$_SESSION['usuario_id']]);
        } else {
            $stmt = $db->prepare('SELECT SUM(cantidad) FROM carrito WHERE sesion_id = ?');
            $stmt->execute([$_SESSION['carrito_sesion_id']]);
        }
        return (int) ($stmt->fetchColumn() ?? 0);
    } catch (PDOException $e) {
        return 0;
    }
}

/**
 * Obtiene el total del carrito del usuario/sesión.
 * Calcula correctamente el precio (oferta vs normal).
 */
function cart_total(): float
{
    try {
        $db = db();
        $priceSql = 'CASE WHEN p.precio_oferta IS NOT NULL AND p.precio_oferta > 0 AND p.precio_oferta < p.precio
                          THEN p.precio_oferta ELSE p.precio END';
        if (is_logged_in()) {
            $stmt = $db->prepare(
                "SELECT COALESCE(SUM(({$priceSql}) * c.cantidad), 0)
                 FROM carrito c
                 JOIN productos p ON c.producto_id = p.id
                 WHERE c.usuario_id = ? AND p.activo = 1"
            );
            $stmt->execute([$_SESSION['usuario_id']]);
        } else {
            $stmt = $db->prepare(
                "SELECT COALESCE(SUM(({$priceSql}) * c.cantidad), 0)
                 FROM carrito c
                 JOIN productos p ON c.producto_id = p.id
                 WHERE c.sesion_id = ? AND p.activo = 1"
            );
            $stmt->execute([$_SESSION['carrito_sesion_id'] ?? '']);
        }
        return (float) $stmt->fetchColumn();
    } catch (PDOException $e) {
        return 0.0;
    }
}

/**
 * Carga dinámica de configuraciones desde la tabla `configuraciones`.
 * Define constantes PHP con cada clave encontrada, en mayúsculas.
 * Los fallbacks garantizan valores por defecto si la DB no está disponible.
 */
function init_db_config(): void
{
    try {
        foreach (load_db_config() as $k => $v) {
            $name = strtoupper($k);
            if (!defined($name)) {
                define($name, $v);
            }
        }
    } catch (Throwable $e) {
        // Sin conexión o tabla inexistente — se usarán los fallbacks
    }

    // Fallbacks: garantizan que siempre existan las constantes mínimas
    $fallbacks = [
        'TIENDA_NOMBRE'    => 'TechShop Perú',
        'TIENDA_SLOGAN'    => 'Tecnología al alcance de todo el Perú',
        'TIENDA_EMAIL'     => 'ventas@techshop.pe',
        'TIENDA_TELEFONO'  => '+51 1 500-5000',
        'TIENDA_WHATSAPP'  => '+51987654321',
        'TIENDA_DIRECCION' => 'Av. Javier Prado 1234, San Isidro, Lima',
        'TIENDA_RUC'       => '20123456789',
        'TIENDA_FACEBOOK'  => 'https://www.facebook.com/techshopperu',
        'TIENDA_INSTAGRAM' => 'https://www.instagram.com/techshopperu',
        'TIENDA_TIKTOK'    => 'https://www.tiktok.com/@techshopperu',
        'TIENDA_YOUTUBE'   => 'https://www.youtube.com/@techshopperu',
        'MONEDA'           => 'S/',
        'IGV_PORCENTAJE'   => '18',
        // SMTP sin configurar = usar mail() nativo del hosting (enviar_email lo maneja).
        'SMTP_HOST'        => '',
        'SMTP_PORT'        => '587',
        'SMTP_USER'        => '',
        'SMTP_PASS'        => '',
        'PAYPAL_CLIENT_ID' => '',
        'PAYPAL_CLIENT_SECRET' => '',
        'PAYPAL_MODE' => 'sandbox',
        'PAYPAL_USD_RATE' => '3.75',
    ];
    foreach ($fallbacks as $k => $v) {
        if (!defined($k)) {
            define($k, $v);
        }
    }
}

init_db_config();

/**
 * Recalcula la calificación promedio y el contador de reseñas de un producto
 * usando solo sus reseñas aprobadas (activo = 1).
 *
 * NULLIF(COUNT(*), 0) devuelve NULL cuando no hay reseñas aprobadas, de modo
 * que COALESCE conserve los valores actuales del producto: un envío en
 * moderación (activo = 0) o la ocultación de una reseña no deben borrar la
 * calificación ni el contador ya existentes.
 */
function recalc_producto_rating(PDO $db, int $producto_id): void
{
    $db->prepare(
        'UPDATE productos p
         SET calificacion = COALESCE((SELECT AVG(calificacion) FROM testimonios WHERE producto_id = ? AND activo = 1), p.calificacion),
             num_calificaciones = COALESCE((SELECT NULLIF(COUNT(*), 0) FROM testimonios WHERE producto_id = ? AND activo = 1), p.num_calificaciones)
         WHERE p.id = ?'
    )->execute([$producto_id, $producto_id, $producto_id]);
}

/**
 * Registra una acción de auditoría en la tabla `admin_audit_log` (Mejora 13).
 * 
 * @param string $accion    Nombre corto de la acción (ej: 'stock_update', 'delete_product')
 * @param string $tabla     Nombre de la tabla afectada
 * @param int    $registro_id  ID del registro afectado
 * @param array  $datos     Datos antes/después (opcional)
 */
function audit_log(string $accion, string $tabla, int $registro_id = 0, array $datos = []): void
{
    try {
        $db = db();
        $db->exec("CREATE TABLE IF NOT EXISTS admin_audit_log (
            id INT AUTO_INCREMENT PRIMARY KEY,
            usuario_id INT DEFAULT NULL,
            accion VARCHAR(100) NOT NULL,
            tabla VARCHAR(100) NOT NULL,
            registro_id INT DEFAULT 0,
            datos JSON DEFAULT NULL,
            ip VARCHAR(64) DEFAULT NULL,
            creado_en TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            KEY idx_tabla_reg (tabla, registro_id),
            KEY idx_usuario (usuario_id),
            KEY idx_creado (creado_en)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");

        $db->prepare("INSERT INTO admin_audit_log (usuario_id, accion, tabla, registro_id, datos, ip) VALUES (?, ?, ?, ?, ?, ?)")
            ->execute([
                $_SESSION['usuario_id'] ?? null,
                $accion,
                $tabla,
                $registro_id,
                !empty($datos) ? json_encode($datos, JSON_UNESCAPED_UNICODE) : null,
                $_SERVER['REMOTE_ADDR'] ?? null,
            ]);
    } catch (Throwable $e) {
        // No interrumpir la operación principal si falla el log
    }
}
