<?php
/**
 * Module - Application module registry and management library.
 *
 * A module is described by a module.php file in mon-appli/modules/<name>/.
 * Descriptor keys:
 *   name         string         Unique identifier (= directory name)
 *   label        string         Display label (English fallback)
 *   description  string         Short description
 *   version      string         Semver string
 *   roles        array<string,string>  Map <role_name> => <human label>.
 *                                Roles are upserted into table `roles`
 *                                at register time (idempotent).
 *   depends_on   string[]       Names of modules that must be active for
 *                                this one to be activated.
 *   on_enable    string|null    Path to a PHP file that returns a callable
 *                                `function ($db, ?string &$error): bool`
 *                                executed at activation; failure aborts.
 *   config_url   string|null    URL of the module's config page (also used
 *                                as redirect target if on_enable fails).
 *   nav_url      string|null    URL to add in the main nav when active
 *   nav_label    string|null    Nav link label (English fallback)
 *   nav_icon     string|null    Emoji / icon for the nav link
 */

class Module
{
    private static ?Module $instance = null;

    /** @var array<string, array> Registered module descriptors keyed by name */
    private array $registered = [];

    /** @var array<string, array> DB rows keyed by module name */
    private array $dbRows = [];

    private Database $db;

    private function __construct()
    {
        $this->db = Database::getInstance();
        $this->loadDbRows();
    }

    public static function getInstance(): Module
    {
        if (self::$instance === null) {
            self::$instance = new Module();
        }
        return self::$instance;
    }

    // -------------------------------------------------------------------------
    // Registration (called during bootstrap)
    // -------------------------------------------------------------------------

    /**
     * Register a module from its descriptor array.
     * Also upserts the module into the DB (preserves existing active/config).
     */
    public function register(array $descriptor): void
    {
        $name = $descriptor['name'] ?? '';
        if ($name === '') {
            return;
        }

        $this->registered[$name] = $descriptor;

        // Insert into DB if not already present
        $this->db->execute(
            "INSERT OR IGNORE INTO modules (name, label, description, active, config)
             VALUES (?, ?, ?, 0, NULL)",
            [
                $name,
                $descriptor['label']       ?? $name,
                $descriptor['description'] ?? '',
            ]
        );

        // Module découvert pour la première fois (ajout d'un module) : les
        // lignes DB ont été chargées avant cet INSERT, donc son état n'est pas
        // encore en cache. Sans ce rattrapage, getAll() renvoie un descripteur
        // sans clé `active` pour toute la requête qui suit l'ajout.
        if (!isset($this->dbRows[$name])) {
            $row = $this->db->fetchOne("SELECT * FROM modules WHERE name = ?", [$name]);
            if ($row) {
                $this->dbRows[$name] = $row;
            }
        }

        // Synchroniser les rôles déclarés par le module dans la table 'roles'.
        // Idempotent (INSERT OR IGNORE) — les libellés existants ne sont pas écrasés.
        // Les modules sont la source de vérité pour leurs rôles.
        foreach (($descriptor['roles'] ?? []) as $roleName => $roleLabel) {
            $this->db->execute(
                "INSERT OR IGNORE INTO roles (name, label, module, is_system)
                 VALUES (?, ?, ?, 0)",
                [$roleName, $roleLabel, $name]
            );
        }
    }

    /**
     * Rafraîchit les métadonnées d'un module déjà enregistré depuis son
     * descripteur — utilisé après une mise à jour de module (nouveau ZIP
     * déposé sur un module existant, cf. admin/modules.php).
     *
     * Nécessaire parce que getAll()/get() font array_merge(descripteur, ligne DB)
     * — la ligne DB gagne — et que register() est en INSERT OR IGNORE : sans
     * cette méthode, un label/description livré par la nouvelle version ne
     * s'afficherait jamais.
     *
     * NE TOUCHE PAS : `active`, `config`, ni la table `user_roles`. Les rôles
     * disparus du nouveau descripteur sont volontairement laissés en base —
     * les supprimer casserait les affectations utilisateurs (FK ON DELETE CASCADE).
     */
    public function syncDescriptor(array $descriptor): void
    {
        $name = $descriptor['name'] ?? '';
        if ($name === '' || !isset($this->dbRows[$name])) {
            return;
        }

        $this->registered[$name] = $descriptor;

        $this->db->execute(
            "UPDATE modules SET label = ?, description = ? WHERE name = ?",
            [
                $descriptor['label']       ?? $name,
                $descriptor['description'] ?? '',
                $name,
            ]
        );

        foreach (($descriptor['roles'] ?? []) as $roleName => $roleLabel) {
            $this->db->execute(
                "INSERT OR IGNORE INTO roles (name, label, module, is_system)
                 VALUES (?, ?, ?, 0)",
                [$roleName, $roleLabel, $name]
            );
            $this->db->execute(
                "UPDATE roles SET label = ? WHERE name = ? AND module = ?",
                [$roleLabel, $roleName, $name]
            );
        }

        $this->reloadDbRows();
    }

    // -------------------------------------------------------------------------
    // Queries
    // -------------------------------------------------------------------------

    /** All registered modules merged with their DB state. */
    public function getAll(): array
    {
        $result = [];
        foreach ($this->registered as $name => $desc) {
            $result[$name] = array_merge($desc, $this->dbRows[$name] ?? []);
        }
        return $result;
    }

    public function get(string $name): ?array
    {
        if (!isset($this->registered[$name])) {
            return null;
        }
        return array_merge($this->registered[$name], $this->dbRows[$name] ?? []);
    }

    public function isActive(string $name): bool
    {
        return (bool)($this->dbRows[$name]['active'] ?? 0);
    }

    public function isRegistered(string $name): bool
    {
        return isset($this->registered[$name]);
    }

    /**
     * Returns true if the user has at least one role for the module
     * (either <name>_user or <name>_admin) AND the module is active.
     * app_admin court-circuite le check (admin de tout).
     */
    public function canAccess(string $name, ?array $user): bool
    {
        if (!$this->isActive($name) || $user === null) return false;

        $roles = $user['roles'] ?? [];
        if (in_array(ROLE_APP_ADMIN, $roles, true)) return true;
        if (in_array($name . '_admin', $roles, true)) return true;
        if (in_array($name . '_user',  $roles, true)) return true;

        return false;
    }

    /**
     * True si l'utilisateur peut accéder à la **configuration** d'un module
     * (page admin / paramètres). Ne couvre QUE les rôles admin (jamais les users).
     * Utilisé pour gating des pages /modules/<X>/admin.php.
     */
    public function canAccessAsAdmin(string $name, ?array $user): bool
    {
        if (!$this->isRegistered($name) || $user === null) return false;
        $roles = $user['roles'] ?? [];
        if (in_array(ROLE_APP_ADMIN, $roles, true)) return true;
        return in_array($name . '_admin', $roles, true);
    }

    /**
     * Nav links for active modules the user can access.
     * Returns array of ['url'=>..., 'label'=>..., 'icon'=>...].
     */
    public function getNavLinks(?array $user): array
    {
        $links = [];
        foreach ($this->registered as $name => $desc) {
            if (!$this->canAccess($name, $user)) {
                continue;
            }
            if (!empty($desc['nav_url'])) {
                $links[] = [
                    'url'      => $desc['nav_url'],
                    'label'    => $desc['nav_label'] ?? $desc['label'] ?? $name,
                    'icon'     => $desc['nav_icon']  ?? '',
                    'icon_img' => $desc['nav_icon_img'] ?? '',
                    'icon_fa'  => $desc['nav_icon_fa'] ?? '',
                ];
            }
        }
        return $links;
    }

    // -------------------------------------------------------------------------
    // Admin actions
    // -------------------------------------------------------------------------

    public function setActive(string $name, bool $active): void
    {
        $this->db->execute(
            "UPDATE modules SET active = ? WHERE name = ?",
            [(int)$active, $name]
        );
        if (isset($this->dbRows[$name])) {
            $this->dbRows[$name]['active'] = (int)$active;
        }

        Logger::getInstance()->info(
            'Module ' . ($active ? 'enabled' : 'disabled'),
            ['module' => $name]
        );
    }

    public function getConfig(string $name): array
    {
        $file = self::configDir() . '/' . $name . '.json';
        if (file_exists($file)) {
            $data = json_decode(file_get_contents($file), true);
            return is_array($data) ? $data : [];
        }

        $raw = $this->dbRows[$name]['config'] ?? null;
        if ($raw !== null && $raw !== '') {
            $config = json_decode($raw, true) ?? [];
            if (!empty($config)) {
                $this->writeConfigFile($name, $config);
                $this->clearDbConfig($name);
            }
            return $config;
        }

        return [];
    }

    public function updateConfig(string $name, array $config): void
    {
        $this->writeConfigFile($name, $config);
        $this->clearDbConfig($name);
    }

    /**
     * Migrate all module configs from DB column to JSON files.
     * Idempotent — skips modules that already have a JSON file.
     * Called at bootstrap and after backup restore.
     */
    public function migrateConfigsFromDb(): void
    {
        $dir = self::configDir();
        foreach ($this->dbRows as $name => $row) {
            $raw = $row['config'] ?? null;
            if ($raw === null || $raw === '') continue;
            $file = $dir . '/' . $name . '.json';
            if (file_exists($file)) continue;
            $config = json_decode($raw, true);
            if (is_array($config) && !empty($config)) {
                $this->writeConfigFile($name, $config);
                $this->clearDbConfig($name);
            }
        }
    }

    private function writeConfigFile(string $name, array $config): void
    {
        $dir = self::configDir();
        if (!is_dir($dir)) mkdir($dir, 0750, true);
        file_put_contents(
            $dir . '/' . $name . '.json',
            json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n"
        );
    }

    private function clearDbConfig(string $name): void
    {
        if (empty($this->dbRows[$name]['config'] ?? null)) return;
        $this->db->execute("UPDATE modules SET config = NULL WHERE name = ?", [$name]);
        if (isset($this->dbRows[$name])) {
            $this->dbRows[$name]['config'] = null;
        }
    }

    private static function configDir(): string
    {
        return SECURITE_PATH . '/modules';
    }

    // -------------------------------------------------------------------------

    /**
     * Reload DB rows (needed after a backup restore overwrites the DB).
     */
    public function reloadDbRows(): void
    {
        $this->loadDbRows();
    }

    private function loadDbRows(): void
    {
        $rows = $this->db->fetchAll("SELECT * FROM modules");
        foreach ($rows as $row) {
            $this->dbRows[$row['name']] = $row;
        }
    }
}
