<?php
/**
 * modules/soporte/Markdown.php
 * Renderizador Markdown → HTML para la documentación del proyecto.
 *
 * Cubre el subconjunto que usa la documentación: encabezados, listas
 * (anidadas y numeradas), tablas, bloques de código, citas, reglas, enlaces,
 * énfasis y código en línea. No pretende ser CommonMark completo — se prefirió
 * un archivo propio y auditable a incorporar una dependencia externa.
 *
 * Todo el texto se escapa antes de aplicar formato, así que el contenido de
 * los .md no puede inyectar HTML.
 */

final class Markdown
{
    /** Convierte un documento Markdown completo a HTML. */
    public static function render(string $texto): string
    {
        $lineas = preg_split('/\R/', $texto);
        $html   = '';
        $n      = count($lineas);
        $i      = 0;

        while ($i < $n) {
            $linea = $lineas[$i];

            // ── Bloque de código cercado ──────────────────────────────
            if (preg_match('/^```\s*([\w-]*)\s*$/', $linea, $m)) {
                $lang   = $m[1];
                $buffer = [];
                $i++;
                while ($i < $n && !preg_match('/^```\s*$/', $lineas[$i])) {
                    $buffer[] = $lineas[$i];
                    $i++;
                }
                $i++; // cerrar
                $clase = $lang ? ' class="lang-' . htmlspecialchars($lang, ENT_QUOTES) . '"' : '';
                $html .= '<pre><code' . $clase . '>'
                       . htmlspecialchars(implode("\n", $buffer), ENT_QUOTES)
                       . '</code></pre>';
                continue;
            }

            // ── Línea en blanco ───────────────────────────────────────
            if (trim($linea) === '') { $i++; continue; }

            // ── Regla horizontal ──────────────────────────────────────
            if (preg_match('/^(-{3,}|\*{3,}|_{3,})\s*$/', $linea)) {
                $html .= '<hr>';
                $i++;
                continue;
            }

            // ── Encabezado ────────────────────────────────────────────
            if (preg_match('/^(#{1,6})\s+(.*)$/', $linea, $m)) {
                $nivel = strlen($m[1]);
                $texto2 = trim($m[2]);
                $slug   = self::slug($texto2);
                $html  .= "<h{$nivel} id=\"{$slug}\">" . self::inline($texto2) . "</h{$nivel}>";
                $i++;
                continue;
            }

            // ── Tabla ─────────────────────────────────────────────────
            if (strpos($linea, '|') !== false
                && isset($lineas[$i + 1])
                && preg_match('/^\s*\|?[\s:|-]+\|[\s:|-]*$/', $lineas[$i + 1])) {
                [$tabla, $i] = self::tabla($lineas, $i);
                $html .= $tabla;
                continue;
            }

            // ── Cita ──────────────────────────────────────────────────
            if (preg_match('/^>\s?(.*)$/', $linea)) {
                $buffer = [];
                while ($i < $n && preg_match('/^>\s?(.*)$/', $lineas[$i], $m2)) {
                    $buffer[] = $m2[1];
                    $i++;
                }
                $html .= '<blockquote>' . self::render(implode("\n", $buffer)) . '</blockquote>';
                continue;
            }

            // ── Lista (con o sin numerar, admite anidación) ───────────
            if (preg_match('/^(\s*)([-*+]|\d+\.)\s+/', $linea)) {
                [$lista, $i] = self::lista($lineas, $i, 0);
                $html .= $lista;
                continue;
            }

            // ── Párrafo ───────────────────────────────────────────────
            $buffer = [];
            while ($i < $n
                   && trim($lineas[$i]) !== ''
                   && !preg_match('/^(#{1,6}\s|```|>|\s*([-*+]|\d+\.)\s|(-{3,}|\*{3,}|_{3,})\s*$)/', $lineas[$i])
                   && !(strpos($lineas[$i], '|') !== false
                        && isset($lineas[$i + 1])
                        && preg_match('/^\s*\|?[\s:|-]+\|[\s:|-]*$/', $lineas[$i + 1]))) {
                $buffer[] = $lineas[$i];
                $i++;
            }
            if ($buffer) $html .= '<p>' . self::inline(implode(' ', $buffer)) . '</p>';
        }

        return $html;
    }

    /** Construye una lista, recursivamente para los niveles anidados. */
    private static function lista(array $lineas, int $i, int $sangriaBase): array
    {
        $n         = count($lineas);
        preg_match('/^(\s*)([-*+]|\d+\.)\s+/', $lineas[$i], $m0);
        $ordenada  = !in_array($m0[2], ['-', '*', '+'], true);
        $tag       = $ordenada ? 'ol' : 'ul';
        $html      = "<{$tag}>";

        while ($i < $n) {
            if (trim($lineas[$i]) === '') {
                // Una línea vacía corta la lista salvo que siga otro ítem
                if (isset($lineas[$i + 1]) && preg_match('/^(\s*)([-*+]|\d+\.)\s+/', $lineas[$i + 1])) {
                    $i++;
                    continue;
                }
                break;
            }
            if (!preg_match('/^(\s*)([-*+]|\d+\.)\s+(.*)$/', $lineas[$i], $m)) break;

            $sangria = strlen($m[1]);
            if ($sangria < $sangriaBase) break;

            if ($sangria > $sangriaBase) {
                [$sub, $i] = self::lista($lineas, $i, $sangria);
                // Colgar la sublista del último ítem abierto
                $html = preg_replace('/<\/li>$/', '', $html) . $sub . '</li>';
                continue;
            }

            $html .= '<li>' . self::inline($m[3]) . '</li>';
            $i++;
        }

        return [$html . "</{$tag}>", $i];
    }

    /** Construye una tabla a partir de la fila de encabezado. */
    private static function tabla(array $lineas, int $i): array
    {
        $n     = count($lineas);
        $celdas = fn(string $l) => array_map('trim', explode('|', trim($l, " \t|")));

        $encabezado = $celdas($lineas[$i]);
        $alineacion = array_map(function ($c) {
            $c = trim($c);
            if (str_starts_with($c, ':') && str_ends_with($c, ':')) return 'center';
            if (str_ends_with($c, ':'))                             return 'right';
            return 'left';
        }, $celdas($lineas[$i + 1]));
        $i += 2;

        $html = '<div class="tabla-scroll"><table><thead><tr>';
        foreach ($encabezado as $k => $c) {
            $a = $alineacion[$k] ?? 'left';
            $html .= '<th style="text-align:' . $a . '">' . self::inline($c) . '</th>';
        }
        $html .= '</tr></thead><tbody>';

        while ($i < $n && trim($lineas[$i]) !== '' && strpos($lineas[$i], '|') !== false) {
            $fila = $celdas($lineas[$i]);
            $html .= '<tr>';
            foreach ($encabezado as $k => $_) {
                $a = $alineacion[$k] ?? 'left';
                $html .= '<td style="text-align:' . $a . '">' . self::inline($fila[$k] ?? '') . '</td>';
            }
            $html .= '</tr>';
            $i++;
        }

        return [$html . '</tbody></table></div>', $i];
    }

    /**
     * Formato dentro de una línea. Se escapa primero y el código en línea se
     * aparta con marcadores para que su contenido no reciba más formato.
     */
    private static function inline(string $texto): string
    {
        $codigos = [];
        $texto = preg_replace_callback('/`([^`]+)`/', function ($m) use (&$codigos) {
            $codigos[] = '<code>' . htmlspecialchars($m[1], ENT_QUOTES) . '</code>';
            return "\x00" . (count($codigos) - 1) . "\x00";
        }, $texto);

        $texto = htmlspecialchars($texto, ENT_QUOTES);

        // Enlaces [texto](destino) — solo http(s), rutas internas y anclas
        $texto = preg_replace_callback(
            '/\[([^\]]+)\]\(([^)\s]+)\)/',
            function ($m) {
                $url = html_entity_decode($m[2], ENT_QUOTES);
                if (!preg_match('#^(https?://|/|\?|\#)#', $url)) return $m[0];
                $ext = str_starts_with($url, 'http') ? ' target="_blank" rel="noopener"' : '';
                return '<a href="' . htmlspecialchars($url, ENT_QUOTES) . '"' . $ext . '>' . $m[1] . '</a>';
            },
            $texto
        );

        $texto = preg_replace('/\*\*([^*]+)\*\*/', '<strong>$1</strong>', $texto);
        $texto = preg_replace('/(?<![\w*])\*([^*\n]+)\*(?![\w*])/', '<em>$1</em>', $texto);
        $texto = preg_replace('/(?<![\w_])_([^_\n]+)_(?![\w_])/', '<em>$1</em>', $texto);

        // Restaurar código en línea
        return preg_replace_callback('/\x00(\d+)\x00/', fn($m) => $codigos[(int)$m[1]] ?? '', $texto);
    }

    /** Ancla estable para un encabezado. */
    public static function slug(string $texto): string
    {
        $t = strtr(mb_strtolower(strip_tags($texto)), [
            'á'=>'a','é'=>'e','í'=>'i','ó'=>'o','ú'=>'u','ñ'=>'n','ü'=>'u',
        ]);
        $t = preg_replace('/[^a-z0-9]+/', '-', $t);
        return trim($t, '-');
    }

    /** Extrae los encabezados h2/h3 para la tabla de contenidos lateral. */
    public static function indice(string $texto): array
    {
        $out = [];
        foreach (preg_split('/\R/', $texto) as $l) {
            if (preg_match('/^(#{2,3})\s+(.*)$/', $l, $m)) {
                $t = trim($m[2]);
                $out[] = ['nivel' => strlen($m[1]), 'texto' => $t, 'slug' => self::slug($t)];
            }
        }
        return $out;
    }
}
