<?php
declare(strict_types=1);

namespace Celena\Plugin\Handbook\Support;

/**
 * Расширенный безопасный Markdown-рендерер для раздела документации.
 *
 * В отличие от ядрового Celena\Core\Support\Markdown добавляет:
 *  - GFM-таблицы (| ... | + строка-сепаратор |---|:--:|);
 *  - вложенные списки по отступам (2/4 пробела);
 *  - якоря (id) у заголовков и сбор оглавления (TOC) для правой колонки.
 *
 * Ядро НЕ трогаем (оно используется на /p/{slug} и в админ-доках) — это
 * изолированная копия с расширениями, чтобы исключить регрессии.
 *
 * HTML из исходника экранируется (XSS невозможен), кроме fenced-блоков,
 * где экранируется только содержимое.
 */
final class DocMarkdown
{
    /**
     * @return array{html:string, toc:list<array{level:int,text:string,id:string}>}
     */
    public static function render(string $source): array
    {
        $toc = [];
        $slugCounts = [];
        $src = str_replace(["\r\n", "\r"], "\n", $source);

        // 1. Вырезаем fenced-блоки (их содержимое не трогаем).
        $fences = [];
        $src = preg_replace_callback('/```([a-zA-Z0-9_\-]*)\n([\s\S]*?)```/u', static function (array $m) use (&$fences): string {
            $placeholder = "\x01FENCE" . count($fences) . "\x02";
            $lang = htmlspecialchars($m[1], ENT_QUOTES, 'UTF-8');
            $code = htmlspecialchars($m[2], ENT_QUOTES, 'UTF-8');
            $cls = $lang !== '' ? ' class="language-' . $lang . '"' : '';
            $fences[] = '<pre><code' . $cls . '>' . $code . '</code></pre>';
            return $placeholder;
        }, $src) ?? $src;

        // 2. Экранируем остальной HTML.
        $src = htmlspecialchars($src, ENT_QUOTES, 'UTF-8');

        // 3. Инлайн-разметка по всему документу (картинки/ссылки/жирный/курсив/код).
        $src = self::inline($src);

        // 4. Блочная обработка.
        $lines = explode("\n", $src);
        $count = count($lines);
        $out = [];
        $paragraph = [];
        $i = 0;

        $flushParagraph = static function () use (&$out, &$paragraph): void {
            if ($paragraph !== []) {
                $out[] = '<p>' . implode("<br>\n", $paragraph) . '</p>';
                $paragraph = [];
            }
        };

        while ($i < $count) {
            $line = $lines[$i];
            $trim = trim($line);

            if ($trim === '') {
                $flushParagraph();
                $i++;
                continue;
            }

            // Плейсхолдер fenced-блока — самостоятельный блок (не заворачивать в <p>).
            if (preg_match('/^\x01FENCE\d+\x02$/', $trim)) {
                $flushParagraph();
                $out[] = $trim;
                $i++;
                continue;
            }

            // Заголовки + якорь + TOC (h2/h3).
            if (preg_match('/^(#{1,6})\s+(.+)$/u', $trim, $m)) {
                $flushParagraph();
                $level = strlen($m[1]);
                $text = trim($m[2]);
                $id = self::slug($text, $slugCounts);
                if ($level >= 2 && $level <= 3) {
                    $toc[] = ['level' => $level, 'text' => self::plain($text), 'id' => $id];
                }
                $out[] = '<h' . $level . ' id="' . $id . '">' . $text . '</h' . $level . '>';
                $i++;
                continue;
            }

            // Горизонтальная линия.
            if (preg_match('/^(-{3,}|_{3,}|\*{3,})$/', $trim)) {
                $flushParagraph();
                $out[] = '<hr>';
                $i++;
                continue;
            }

            // GFM-таблица: текущая строка с «|» + следующая — сепаратор.
            if (str_contains($trim, '|') && isset($lines[$i + 1]) && self::isTableSeparator(trim($lines[$i + 1]))) {
                $flushParagraph();
                [$html, $consumed] = self::table($lines, $i);
                $out[] = $html;
                $i += $consumed;
                continue;
            }

            // Цитата.
            if (preg_match('/^&gt;\s?(.*)$/', $trim)) {
                $flushParagraph();
                $quote = [];
                while ($i < $count && preg_match('/^&gt;\s?(.*)$/', trim($lines[$i]), $mm)) {
                    $quote[] = $mm[1];
                    $i++;
                }
                $out[] = '<blockquote>' . implode("<br>\n", $quote) . '</blockquote>';
                continue;
            }

            // Списки (вложенные по отступам).
            if (preg_match('/^\s*([-*]|\d+\.)\s+/', $line)) {
                $flushParagraph();
                [$html, $consumed] = self::lists($lines, $i);
                $out[] = $html;
                $i += $consumed;
                continue;
            }

            $paragraph[] = $trim;
            $i++;
        }
        $flushParagraph();

        $html = implode("\n", $out);

        // 5. Возвращаем fenced-блоки.
        foreach ($fences as $k => $block) {
            $html = str_replace("\x01FENCE$k\x02", $block, $html);
        }

        return ['html' => $html, 'toc' => $toc];
    }

    private static function inline(string $s): string
    {
        $s = preg_replace('/!\[([^\]]*)\]\(([^)\s]+)(?:\s+&quot;([^&]+)&quot;)?\)/u', '<img src="$2" alt="$1">', $s) ?? $s;
        $s = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/u', '<a href="$2">$1</a>', $s) ?? $s;
        $s = preg_replace('/\*\*(.+?)\*\*/u', '<strong>$1</strong>', $s) ?? $s;
        $s = preg_replace('/(?<![\w*])\*([^*\n]+)\*(?![\w*])/u', '<em>$1</em>', $s) ?? $s;
        $s = preg_replace('/`([^`\n]+)`/u', '<code>$1</code>', $s) ?? $s;
        return $s;
    }

    /** Сепаратор таблицы: каждая ячейка вида :---, :--:, ---:, --- */
    private static function isTableSeparator(string $s): bool
    {
        if (!str_contains($s, '-')) {
            return false;
        }
        $s = trim($s, '| ');
        if ($s === '') {
            return false;
        }
        foreach (explode('|', $s) as $cell) {
            if (!preg_match('/^\s*:?-{1,}:?\s*$/', $cell)) {
                return false;
            }
        }
        return true;
    }

    /**
     * @param string[] $lines
     * @return array{0:string,1:int} HTML + число поглощённых строк
     */
    private static function table(array $lines, int $start): array
    {
        $header = self::cells($lines[$start]);
        $aligns = self::aligns($lines[$start + 1]);
        $count = count($lines);
        $i = $start + 2;
        $rows = [];
        while ($i < $count) {
            $t = trim($lines[$i]);
            if ($t === '' || !str_contains($t, '|')) {
                break;
            }
            $rows[] = self::cells($lines[$i]);
            $i++;
        }

        $html = '<div class="hb-table-wrap"><table><thead><tr>';
        foreach ($header as $idx => $h) {
            $style = ($aligns[$idx] ?? '') !== '' ? ' style="text-align:' . $aligns[$idx] . '"' : '';
            $html .= '<th' . $style . '>' . $h . '</th>';
        }
        $html .= '</tr></thead><tbody>';
        foreach ($rows as $row) {
            $html .= '<tr>';
            foreach ($header as $idx => $_) {
                $style = ($aligns[$idx] ?? '') !== '' ? ' style="text-align:' . $aligns[$idx] . '"' : '';
                $html .= '<td' . $style . '>' . ($row[$idx] ?? '') . '</td>';
            }
            $html .= '</tr>';
        }
        $html .= '</tbody></table></div>';

        return [$html, $i - $start];
    }

    /** @return string[] */
    private static function cells(string $line): array
    {
        $line = trim($line);
        $line = preg_replace('/^\|/', '', $line) ?? $line;
        $line = preg_replace('/\|$/', '', $line) ?? $line;
        return array_map('trim', explode('|', $line));
    }

    /** @return string[] */
    private static function aligns(string $line): array
    {
        $out = [];
        foreach (self::cells($line) as $cell) {
            $cell = trim($cell);
            $left = str_starts_with($cell, ':');
            $right = str_ends_with($cell, ':');
            $out[] = $left && $right ? 'center' : ($right ? 'right' : ($left ? 'left' : ''));
        }
        return $out;
    }

    /**
     * Вложенные списки по отступам. Поглощает непрерывный блок строк-пунктов.
     *
     * @param string[] $lines
     * @return array{0:string,1:int}
     */
    private static function lists(array $lines, int $start): array
    {
        $count = count($lines);
        $i = $start;
        $html = '';
        /** @var list<array{indent:int,type:string}> $stack */
        $stack = [];

        while ($i < $count) {
            if (!preg_match('/^(\s*)([-*]|\d+\.)\s+(.*)$/u', $lines[$i], $m)) {
                break;
            }
            $indent = strlen($m[1]);
            $type = ($m[2] === '-' || $m[2] === '*') ? 'ul' : 'ol';
            $content = $m[3];

            if ($stack === []) {
                $stack[] = ['indent' => $indent, 'type' => $type];
                $html .= '<' . $type . '><li>' . $content;
            } else {
                $top = $stack[count($stack) - 1];
                if ($indent > $top['indent']) {
                    // Вложенный список внутри открытого <li> (его не закрываем).
                    $stack[] = ['indent' => $indent, 'type' => $type];
                    $html .= '<' . $type . '><li>' . $content;
                } elseif ($indent < $top['indent']) {
                    while (count($stack) > 1 && $indent < $stack[count($stack) - 1]['indent']) {
                        $popped = array_pop($stack);
                        $html .= '</li></' . $popped['type'] . '>';
                    }
                    $html .= '</li><li>' . $content;
                } else {
                    $html .= '</li><li>' . $content;
                }
            }
            $i++;
        }

        while ($stack !== []) {
            $popped = array_pop($stack);
            $html .= '</li></' . $popped['type'] . '>';
        }

        return [$html, $i - $start];
    }

    /** @param array<string,int> $counts */
    private static function slug(string $text, array &$counts): string
    {
        $base = self::plain($text);
        $base = mb_strtolower($base, 'UTF-8');
        $base = preg_replace('/[^\p{L}\p{N}]+/u', '-', $base) ?? $base;
        $base = trim($base, '-');
        if ($base === '') {
            $base = 'section';
        }
        $n = $counts[$base] ?? 0;
        $counts[$base] = $n + 1;
        return $n === 0 ? $base : $base . '-' . $n;
    }

    /** Текст без HTML-тегов и сущностей — для TOC и slug. */
    private static function plain(string $s): string
    {
        return html_entity_decode(strip_tags($s), ENT_QUOTES, 'UTF-8');
    }
}
