<?php
/**
 * core/ModuleRegistry.php
 * Catálogo dinámico de módulos del ERP.
 *
 * Fuentes de datos (prioridad en orden):
 *   1. Base de datos — tabla system_modules (si la migración 004 se ejecutó)
 *   2. Archivos modules/{slug}/module.php (escaneo de la carpeta modules/)
 *   3. Constante SYSTEM_MODULES de config.php (fallback mínimo)
 *
 * Los datos de BD prevalecen sobre los descriptores locales para los campos
 * is_active y sort_order (el administrador puede cambiarlos via UI).
 *
 * CACHÉ: estática por request (no Redis necesario).
 */

class ModuleRegistry
{
    /** @var array<string, array>|null Cache del registro completo */
    private static ?array $registry = null;

    /** Orden de visualización de categorías en el sidebar */
    private const CATEGORY_ORDER = ['bot', 'lab', 'formularios', 'turnero', 'clinico', 'reportes', 'sistema'];

    /** Etiquetas e íconos de cada categoría */
    private const CATEGORIES = [
        'bot'        => ['label' => 'WhatsApp',   'icon' => 'fab fa-whatsapp'],
        'lab'        => ['label' => 'Domicilios', 'icon' => 'fas fa-house-medical'],
        'formularios'=> ['label' => 'Formulario', 'icon' => 'fas fa-file-invoice'],
        'turnero'    => ['label' => 'Turnero',    'icon' => 'fas fa-ticket-alt'],
        'clinico'    => ['label' => 'Clínico',    'icon' => 'fas fa-vials'],
        'reportes'   => ['label' => 'Reportes',   'icon' => 'fas fa-chart-bar'],
        'sistema'    => ['label' => 'Sistema',    'icon' => 'fas fa-cog'],
    ];

    // ─── API pública ────────────────────────────────────────────────────────

    /**
     * Retorna todos los módulos registrados (activos e inactivos).
     *
     * @return array<string, array>  Indexado por slug.
     */
    public static function getAll(): array
    {
        self::load();
        return self::$registry;
    }

    /**
     * Retorna solo los módulos ACTIVOS que el usuario actual puede ver.
     *
     * IMPORTANTE: consulta role_modules desde BD en tiempo real para que los
     * cambios hechos en lab_usuarios.php se reflejen sin necesidad de re-login.
     *
     * Excepción: superadmin y admin sin role_id tienen acceso total (retrocompat).
     *
     * @return array<string, array>  Indexado por slug.
     */
    public static function forCurrentUser(): array
    {
        self::load();
        $allowed = self::liveUserModules(); // null = acceso total

        $result = [];
        foreach (self::$registry as $slug => $mod) {
            if (!$mod['is_active']) {
                continue;
            }
            // null = superadmin / admin legacy → ve todo
            if ($allowed !== null && !in_array($slug, $allowed, true)) {
                continue;
            }
            $result[$slug] = $mod;
        }
        return $result;
    }

    /**
     * Obtiene los slugs de módulos permitidos para el usuario actual consultando
     * role_modules en BD (ignora la caché de sesión para ser siempre fresco).
     *
     * Retorna null si el usuario tiene acceso total (superadmin / admin legacy).
     *
     * @return string[]|null
     */
    private static function liveUserModules(): ?array
    {
        $role   = $_SESSION['admin_user']['role']    ?? 'admin';
        $roleId = $_SESSION['admin_user']['role_id'] ?? null;

        // Superadmin → acceso total siempre
        if ($role === 'superadmin') {
            return null;
        }

        // Admin sin role_id asignado → retrocompat, acceso total
        if ($role === 'admin' && !$roleId) {
            return null;
        }

        // Sin role_id en cualquier otro rol → usar sesión como fallback
        if (!$roleId) {
            return $_SESSION['admin_user']['modules'] ?? [];
        }

        // Consulta en tiempo real: reflejan cambios en lab_usuarios.php al instante
        try {
            $db   = Database::getInstance();
            $pdo  = $db->getConnection();
            $stmt = $pdo->prepare(
                "SELECT module_slug FROM role_modules WHERE role_id = ?"
            );
            $stmt->execute([(int) $roleId]);
            return array_column($stmt->fetchAll(PDO::FETCH_ASSOC), 'module_slug');
        } catch (Exception $e) {
            error_log("ModuleRegistry::liveUserModules error: " . $e->getMessage());
            // Fallback a sesión si la BD no responde
            return $_SESSION['admin_user']['modules'] ?? [];
        }
    }

    /**
     * Retorna módulos activos agrupados por categoría para el sidebar.
     * Solo incluye módulos accesibles por el usuario actual.
     *
     * @return array  ['bot' => ['label'=>'WhatsApp','icon'=>'...','modules'=>[...]], ...]
     */
    public static function forSidebar(): array
    {
        $userModules = self::forCurrentUser();

        // Agrupar por categoría
        $grouped = [];
        foreach ($userModules as $slug => $mod) {
            $cat = $mod['category'] ?? 'sistema';
            if (!isset($grouped[$cat])) {
                $catMeta    = self::CATEGORIES[$cat] ?? ['label' => ucfirst($cat), 'icon' => 'fas fa-cube'];
                $grouped[$cat] = [
                    'label'   => $catMeta['label'],
                    'icon'    => $catMeta['icon'],
                    'modules' => [],
                ];
            }
            $grouped[$cat]['modules'][$slug] = $mod;
        }

        // Ordenar grupos según CATEGORY_ORDER
        $ordered = [];
        foreach (self::CATEGORY_ORDER as $cat) {
            if (isset($grouped[$cat])) {
                // Ordenar módulos dentro de la categoría por sort_order
                uasort($grouped[$cat]['modules'], fn($a, $b) => $a['sort_order'] <=> $b['sort_order']);
                $ordered[$cat] = $grouped[$cat];
            }
        }
        // Categorías extra no definidas en CATEGORY_ORDER al final
        foreach ($grouped as $cat => $data) {
            if (!isset($ordered[$cat])) {
                $ordered[$cat] = $data;
            }
        }

        return $ordered;
    }

    /**
     * Retorna el descriptor de un módulo por slug, o null si no existe.
     */
    public static function get(string $slug): ?array
    {
        self::load();
        return self::$registry[$slug] ?? null;
    }

    /**
     * Activa o desactiva un módulo en la base de datos.
     *
     * @throws RuntimeException Si no se puede conectar a la BD.
     */
    public static function setActive(string $slug, bool $active): bool
    {
        try {
            $db  = Database::getInstance();
            $pdo = $db->getConnection();
            $stmt = $pdo->prepare(
                "UPDATE system_modules SET is_active = ? WHERE slug = ?"
            );
            $ok = $stmt->execute([(int) $active, $slug]);
            if ($ok) {
                self::$registry = null; // invalidar caché
            }
            return $ok;
        } catch (Exception $e) {
            error_log("ModuleRegistry::setActive error: " . $e->getMessage());
            return false;
        }
    }

    /**
     * Actualiza el sort_order de un módulo.
     */
    public static function setSortOrder(string $slug, int $order): bool
    {
        try {
            $db  = Database::getInstance();
            $pdo = $db->getConnection();
            $stmt = $pdo->prepare(
                "UPDATE system_modules SET sort_order = ? WHERE slug = ?"
            );
            $ok = $stmt->execute([$order, $slug]);
            if ($ok) {
                self::$registry = null;
            }
            return $ok;
        } catch (Exception $e) {
            error_log("ModuleRegistry::setSortOrder error: " . $e->getMessage());
            return false;
        }
    }

    /**
     * Invalida la caché para forzar recarga en la próxima llamada.
     * Útil después de cambios por el administrador.
     */
    public static function flush(): void
    {
        self::$registry = null;
    }

    // ─── Carga interna ──────────────────────────────────────────────────────

    private static function load(): void
    {
        if (self::$registry !== null) {
            return;
        }

        self::$registry = [];

        // 1. Escanear module.php locales para obtener metadatos base
        self::loadFromFiles();

        // 2. Sobreescribir is_active / sort_order desde la BD (si existe)
        self::mergeFromDatabase();

        // 3. Fallback: añadir slugs de SYSTEM_MODULES que no tengan module.php
        self::fillFromConstant();
    }

    /**
     * Escanea modules/{slug}/module.php y carga sus descriptores.
     */
    private static function loadFromFiles(): void
    {
        if (!defined('APP_ROOT')) {
            return;
        }
        $pattern = APP_ROOT . '/modules/*/module.php';
        foreach (glob($pattern) ?: [] as $file) {
            $data = @include $file;
            if (is_array($data) && isset($data['slug'])) {
                self::$registry[$data['slug']] = self::normalize($data);
            }
        }
    }

    /**
     * Lee la tabla system_modules y sobreescribe campos controlados por el admin.
     */
    private static function mergeFromDatabase(): void
    {
        try {
            $db  = Database::getInstance();
            $pdo = $db->getConnection();

            // Verificar si existe la tabla
            $check = $pdo->query("SHOW TABLES LIKE 'system_modules'");
            if (!$check || $check->rowCount() === 0) {
                return;
            }

            $rows = $pdo->query(
                "SELECT slug, name, icon, category, route, is_active, sort_order, oleada, description
                   FROM system_modules ORDER BY sort_order ASC"
            )->fetchAll(PDO::FETCH_ASSOC);

            foreach ($rows as $row) {
                $slug = $row['slug'];
                if (isset(self::$registry[$slug])) {
                    // Actualizar solo los campos que maneja el admin
                    self::$registry[$slug]['is_active']  = (bool)(int)$row['is_active'];
                    self::$registry[$slug]['sort_order'] = (int)$row['sort_order'];
                    // Si en BD hay datos más completos, usar los de BD
                    if (!empty($row['name']))        self::$registry[$slug]['name']     = $row['name'];
                    if (!empty($row['icon']))        self::$registry[$slug]['icon']     = $row['icon'];
                    if (!empty($row['category']))    self::$registry[$slug]['category'] = $row['category'];
                    if (!empty($row['route']))       self::$registry[$slug]['route']    = $row['route'];
                    if (!empty($row['description'])) self::$registry[$slug]['description'] = $row['description'];
                } else {
                    // Módulo solo en BD (sin module.php local)
                    self::$registry[$slug] = self::normalize([
                        'slug'        => $slug,
                        'name'        => $row['name'],
                        'icon'        => $row['icon'],
                        'category'    => $row['category'],
                        'route'       => $row['route'],
                        'is_active'   => (bool)(int)$row['is_active'],
                        'sort_order'  => (int)$row['sort_order'],
                        'oleada'      => (int)$row['oleada'],
                        'description' => $row['description'],
                    ]);
                }
            }
        } catch (Exception $e) {
            // BD no disponible → seguir con datos de archivos
            error_log("ModuleRegistry DB merge error: " . $e->getMessage());
        }
    }

    /**
     * Agrega módulos del array SYSTEM_MODULES que no estén ya registrados.
     * Garantiza compatibilidad si no se ejecutó la migración 004.
     */
    private static function fillFromConstant(): void
    {
        if (!defined('SYSTEM_MODULES')) {
            return;
        }
        foreach (SYSTEM_MODULES as $slug => $name) {
            if (!isset(self::$registry[$slug])) {
                self::$registry[$slug] = self::normalize([
                    'slug'       => $slug,
                    'name'       => $name,
                    'icon'       => 'fas fa-cube',
                    'category'   => 'sistema',
                    'route'      => null,
                    'is_active'  => true,
                    'sort_order' => 99,
                    'oleada'     => 0,
                ]);
            }
        }
    }

    /**
     * Normaliza un descriptor garantizando que todos los campos existen.
     */
    private static function normalize(array $data): array
    {
        return [
            'slug'        => $data['slug']        ?? '',
            'name'        => $data['name']         ?? ucfirst($data['slug'] ?? ''),
            'icon'        => $data['icon']         ?? 'fas fa-cube',
            'category'    => $data['category']     ?? 'sistema',
            'route'       => $data['route']        ?? null,
            'is_active'   => (bool)($data['is_active'] ?? true),
            'sort_order'  => (int)($data['sort_order'] ?? 99),
            'oleada'      => (int)($data['oleada'] ?? 0),
            'description' => $data['description']  ?? '',
            'links'       => $data['links']        ?? [],
        ];
    }
}
