<?php

declare(strict_types=1);


namespace App\Blog\Services;

use App\Models\Post;
use Illuminate\Support\Facades\Cache;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
use League\CommonMark\MarkdownConverter;
use Spatie\MediaLibrary\MediaCollections\Models\Media;

class PostContentRenderer
{
    private MarkdownConverter $converter;

    public function __construct()
    {
        $environment = new Environment([
            'html_input' => 'allow',
            'allow_unsafe_links' => true,
            'max_nesting_level' => 100,
        ]);
        $environment->addExtension(new CommonMarkCoreExtension);
        $environment->addExtension(new GithubFlavoredMarkdownExtension);

        $this->converter = new MarkdownConverter($environment);
    }

    /**
     * 将文章内容按格式渲染为 HTML。
     * markdown -> HTML；html 原样返回。两种格式都会解析附件令牌。
     */
    public function render(Post $post): string
    {
        $content = $post->content_format === 'markdown'
            ? $this->toHtml($post->content)
            : $post->content;

        $html = $this->renderShortcodes($content, $post);

        // 插件钩子：付费内容过滤、AI 处理等
        $html = app(\App\Blog\Support\PluginManager::class)->applyFilters('post.rendered', $html, $post);

        // 兜底：任何情况下不把裸 [paid] 标签输出到页面（如会员插件被停用时）
        return preg_replace('/\[(\/?paid)\]/i', '', $html);
    }

    /**
     * 渲染并提取目录（TOC）。
     *
     * @return array{html: string, toc: array<int, array{id: string, text: string, level: int}>}
     */
    public function renderWithToc(Post $post): array
    {
        $html = $this->render($post);

        $toc = [];
        $index = 0;

        $html = preg_replace_callback(
            '/<h([2-6])([^>]*)>(.*?)<\/h\1>/is',
            function (array $m) use (&$toc, &$index): string {
                $level = (int) $m[1];
                $attrs = $m[2];
                $text = trim(strip_tags($m[3]));
                $id = 'toc-'.(++$index);

                $toc[] = ['id' => $id, 'text' => $text, 'level' => $level];

                // 已存在 id 则保留原 id，否则注入锚点
                if (preg_match('/id="([^"]+)"/', $attrs, $idMatch)) {
                    $toc[array_key_last($toc)]['id'] = $idMatch[1];

                    return $m[0];
                }

                return '<h'.$level.$attrs.' id="'.$id.'">'.$m[3].'</h'.$level.'>';
            },
            $html
        );

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

    public function toHtml(string $markdown): string
    {
        return $this->converter->convert($markdown)->getContent();
    }

    /**
     * 解析附件引用。
     *
     * 兼容两种写法：
     *  - 老 sablog HTML 内容： [attach=xx] / [img=xx]
     *  - markdown 转换导入：  {{attach:xx}} / {{img:xx}}（[] 是 markdown 保留字符，转换时改为令牌）
     *
     * xx 是 sablog attachmentid；找不到时回退到按出现顺序的第 N 个附件。
     */
    public function renderShortcodes(string $html, Post $post): string
    {
        $pattern = '/\[(attach|img)=(\d+)\]|\{\{(attach|img):(\d+)\}\}/';

        if (! preg_match_all($pattern, $html, $matches, PREG_SET_ORDER)) {
            return $html;
        }

        // 短代码引用的是 sablog 全局附件 id，可能属于其他文章，需要全局查
        $byLegacyId = Cache::remember('attach.legacy_ids', now()->addHour(), function () {
            return Media::query()
                ->where('collection_name', 'attachments')
                ->get()
                ->keyBy(fn ($m) => (int) $m->getCustomProperty('legacy_attachmentid'));
        });

        // 当前文章媒体用于索引回退
        $ordered = $post->getMedia('attachments')->values();

        foreach ($matches as $match) {
            $full = $match[0];
            $type = $match[1] !== '' ? $match[1] : $match[3];
            $id = (int) ($match[2] !== '' ? $match[2] : $match[4]);

            $media = $byLegacyId->get($id);

            if (! $media) {
                // 兼容按索引引用的旧写法：第 $id 个附件（从 1 开始）
                $media = $ordered->get($id - 1);
            }

            if (! $media) {
                // 找不到媒体时移除残留短代码，避免把 [attach=999] 当正文显示
                $html = str_replace($full, '', $html);
                continue;
            }

            $replacement = $type === 'img'
                ? sprintf('<img src="%s" alt="%s" loading="lazy" />', $media->getUrl(), e($media->name))
                : sprintf('<a href="%s">%s</a>', $media->getUrl(), e($media->file_name));

            $html = str_replace($full, $replacement, $html);
        }

        return $html;
    }

    /**
     * 把老 sablog HTML 里的 [attach=xx]/[img=xx] 换成 markdown 安全令牌，
     * 供 HTML -> markdown 转换前调用。
     */
    public function toMarkdownSafeTokens(string $html): string
    {
        return preg_replace('/\[(attach|img)=(\d+)\]/', '{{$1:$2}}', $html);
    }
}
