<?php
/**
 * modules/soporte/Generadores.php
 * Secciones de la documentación que se leen del código y de la base de datos
 * en cada carga, en vez de escribirse a mano.
 *
 * El objetivo es que los inventarios (módulos, endpoints, tablas, roles) no
 * puedan quedar desactualizados: si alguien agrega un módulo o una tabla,
 * aparece aquí sin que nadie tenga que acordarse de editar un .md.
 *
 * Se invocan desde los documentos con marcadores en una línea propia:
 *     {{modulos}}  {{endpoints}}  {{tablas}}  {{roles}}  {{servicios}}
 */

final class Generadores
{
    /** Reemplaza los marcadores {{...}} de un documento por su tabla generada. */
    public static function expandir(string $md): string
    {
        return preg_replace_callback('/^\{\{(\w+)\}\}\s*$/m', function ($m) {
            $metodo = 'gen' . ucfirst($m[1]);
            if (!method_exists(self::class, $metodo)) return $m[0];
            try {
                return self::$metodo();
            } catch (\Throwable $e) {
                return '> No se pudo generar esta sección: ' . $e->getMessage();
            }
        }, $md);
    }

    private static function pdo(): PDO
    {
        return Database::getInstance()->getConnection();
    }

    private static function raiz(): string
    {
        return dirname(__DIR__, 2);
    }

    // ── Módulos registrados ───────────────────────────────────────────
    private static function genModulos(): string
    {
        $dirs = glob(self::raiz() . '/modules/*', GLOB_ONLYDIR) ?: [];
        $filas = [];
        foreach ($dirs as $dir) {
            $slug = basename($dir);
            $meta = ['name' => $slug, 'description' => '', 'oleada' => ''];
            $mf = $dir . '/module.php';
            if (is_file($mf)) {
                try {
                    $m = @include $mf;
                    if (is_array($m)) $meta = $m + $meta;
                } catch (\Throwable $e) { /* un module.php con contexto no cargable no debe romper la doc */ }
            }
            $vistas = count(glob($dir . '/views/*.php') ?: []);
            $apis   = count(glob($dir . '/api/*.php')   ?: []);
            $enSys  = defined('SYSTEM_MODULES') && array_key_exists($slug, SYSTEM_MODULES);
            $filas[] = [
                '`' . $slug . '`',
                $meta['name'] ?? $slug,
                $vistas ?: '—',
                $apis ?: '—',
                $enSys ? 'sí' : 'no',
                $meta['description'] ?? '',
            ];
        }
        usort($filas, fn($a, $b) => strcmp($a[0], $b[0]));
        return self::tabla(
            ['Slug', 'Nombre', 'Vistas', 'Endpoints', 'En SYSTEM_MODULES', 'Descripción'],
            $filas
        ) . "\n\n_" . count($filas) . " módulos en `modules/`. Generado del filesystem._\n";
    }

    // ── Endpoints por módulo ──────────────────────────────────────────
    private static function genEndpoints(): string
    {
        $out = '';
        foreach (glob(self::raiz() . '/modules/*/api', GLOB_ONLYDIR) ?: [] as $dir) {
            $slug  = basename(dirname($dir));
            $files = glob($dir . '/*.php') ?: [];
            if (!$files) continue;
            sort($files);

            $filas = [];
            foreach ($files as $f) {
                $nombre = basename($f);
                if (str_starts_with($nombre, '_')) continue; // helpers internos
                $filas[] = ['`' . $nombre . '`', self::resumenPhpDoc($f)];
            }
            if (!$filas) continue;
            $out .= "\n### " . $slug . "\n\n" . self::tabla(['Endpoint', 'Qué hace'], $filas) . "\n";
        }
        return $out ?: '> Sin endpoints.';
    }

    // ── Tablas de la base de datos ────────────────────────────────────
    private static function genTablas(): string
    {
        $rows = self::pdo()->query(
            "SELECT table_name, table_rows, table_comment
               FROM information_schema.tables
              WHERE table_schema = DATABASE() AND table_type = 'BASE TABLE'
              ORDER BY table_name"
        )->fetchAll(PDO::FETCH_ASSOC);

        // Agrupar por prefijo para que se lea por dominio
        $grupos = [];
        foreach ($rows as $r) {
            $t   = $r['table_name'];
            $pfx = str_contains($t, '_') ? explode('_', $t)[0] : 'otras';
            $grupos[$pfx][] = $r;
        }
        ksort($grupos);

        $out = '';
        foreach ($grupos as $pfx => $tablas) {
            $out .= "\n### " . $pfx . "\n\n";
            $filas = [];
            foreach ($tablas as $t) {
                $cols = self::pdo()->prepare(
                    "SELECT COUNT(*) FROM information_schema.columns
                      WHERE table_schema = DATABASE() AND table_name = ?"
                );
                $cols->execute([$t['table_name']]);
                $filas[] = [
                    '`' . $t['table_name'] . '`',
                    (string)(int)$cols->fetchColumn(),
                    number_format((int)$t['table_rows'], 0, ',', '.'),
                    $t['table_comment'] ?: '',
                ];
            }
            $out .= self::tabla(['Tabla', 'Columnas', 'Filas aprox.', 'Comentario'], $filas) . "\n";
        }
        return $out . "\n_" . count($rows) . " tablas. Generado de `information_schema`; el conteo de filas es una estimación de InnoDB._\n";
    }

    // ── Roles y permisos efectivos ────────────────────────────────────
    private static function genRoles(): string
    {
        $roles = self::pdo()->query(
            "SELECT r.id, r.slug, r.name, r.description,
                    (SELECT COUNT(*) FROM admin_users u WHERE u.role_id = r.id AND u.is_active = 1) AS usuarios
               FROM roles r ORDER BY r.slug"
        )->fetchAll(PDO::FETCH_ASSOC);

        $mods = self::pdo()->query(
            "SELECT role_id, module_slug, permission FROM role_modules ORDER BY module_slug"
        )->fetchAll(PDO::FETCH_ASSOC);

        $porRol = [];
        foreach ($mods as $m) {
            $porRol[(int)$m['role_id']][] = $m['module_slug'] . ($m['permission'] === 'read' ? ' _(solo lectura)_' : '');
        }

        $filas = [];
        foreach ($roles as $r) {
            $lista = $porRol[(int)$r['id']] ?? [];
            $filas[] = [
                '`' . $r['slug'] . '`',
                $r['name'] ?: '',
                (string)(int)$r['usuarios'],
                $lista ? implode(', ', $lista) : '—',
            ];
        }
        return self::tabla(['Rol', 'Nombre', 'Usuarios activos', 'Módulos'], $filas)
             . "\n\n_Generado de `roles` y `role_modules`. El acceso efectivo se carga al iniciar sesión desde `role_id`._\n";
    }

    // ── Servicios y clases del núcleo ─────────────────────────────────
    private static function genServicios(): string
    {
        $out = '';
        foreach ([['core', 'Núcleo'], ['services', 'Servicios'], ['classes', 'Clases de dominio']] as [$dir, $tit]) {
            $files = glob(self::raiz() . '/' . $dir . '/*.php') ?: [];
            if (!$files) continue;
            sort($files);
            $filas = [];
            foreach ($files as $f) {
                $filas[] = ['`' . basename($f) . '`', self::resumenPhpDoc($f)];
            }
            $out .= "\n### " . $tit . " (`" . $dir . "/`)\n\n" . self::tabla(['Archivo', 'Responsabilidad'], $filas) . "\n";
        }
        return $out;
    }

    /** Primera línea con contenido del bloque docblock inicial de un archivo. */
    private static function resumenPhpDoc(string $archivo): string
    {
        $fh = @fopen($archivo, 'r');
        if (!$fh) return '';
        $n = 0;
        $ruta = null;
        while (($l = fgets($fh)) !== false && $n++ < 25) {
            $l = trim($l);
            if (!str_starts_with($l, '*')) continue;
            $l = trim(ltrim($l, '*/ '));
            if ($l === '') continue;
            // Saltar la línea que solo repite la ruta del archivo
            if ($ruta === null && str_contains($l, '.php')) { $ruta = $l; continue; }
            if (str_starts_with($l, '@')) break;
            fclose($fh);
            return $l;
        }
        fclose($fh);
        return '';
    }

    /** Arma una tabla Markdown escapando los separadores del contenido. */
    private static function tabla(array $encabezados, array $filas): string
    {
        $esc = fn($v) => str_replace('|', '\\|', (string)$v);
        $md  = '| ' . implode(' | ', array_map($esc, $encabezados)) . " |\n";
        $md .= '|' . str_repeat('---|', count($encabezados)) . "\n";
        foreach ($filas as $f) {
            $md .= '| ' . implode(' | ', array_map($esc, $f)) . " |\n";
        }
        return $md;
    }
}
