<?php

namespace App\Services;

use Illuminate\Support\Facades\Log;
use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;

/**
 * KaTeX 服务端渲染服务
 *
 * 使用 Node.js 的 KaTeX 库在服务端预渲染 LaTeX 公式
 * 避免依赖 Chrome headless 执行 JavaScript
 */
class KatexRenderer
{
    /**
     * Node.js 脚本路径
     */
    private string $scriptPath;

    /**
     * 是否启用缓存
     */
    private bool $cacheEnabled = true;

    /**
     * 缓存前缀
     */
    private const CACHE_PREFIX = 'katex_rendered_';

    /**
     * 缓存时间（秒）
     */
    private const CACHE_TTL = 86400; // 24小时

    public function __construct()
    {
        $this->scriptPath = base_path('scripts/katex-render.mjs');
    }

    /**
     * 渲染 HTML 中的所有 LaTeX 公式
     *
     * @param string $html 包含 LaTeX 公式的 HTML
     * @return string 渲染后的 HTML
     */
    public function renderHtml(string $html): string
    {
        // 检查是否包含需要渲染的公式
        if (!$this->containsLatex($html)) {
            Log::debug('KatexRenderer: HTML 不包含 LaTeX 公式，跳过渲染');
            return $html;
        }

        // 在渲染前修复公式中的实体与 cases 换行问题
        $html = $this->sanitizeLatexInHtml($html);

        // 尝试从缓存获取
        $cacheKey = $this->getCacheKey($html);
        if ($this->cacheEnabled && $cached = cache()->get($cacheKey)) {
            Log::debug('KatexRenderer: 从缓存获取渲染结果');
            return $cached;
        }

        // 调用 Node.js 脚本渲染
        $rendered = $this->callNodeScript($html);

        if (strpos($rendered, 'katex-error') !== false) {
            Log::warning('KatexRenderer: 发现未解析公式(katex-error)', [
                'sample' => $this->extractKatexErrorSnippet($rendered),
            ]);
        }

        // 缓存结果
        if ($this->cacheEnabled && $rendered !== $html) {
            cache()->put($cacheKey, $rendered, self::CACHE_TTL);
        }

        return $rendered;
    }

    /**
     * 检查 HTML 是否包含 LaTeX 公式
     */
    private function containsLatex(string $html): bool
    {
        // 检查常见的 LaTeX 定界符
        return preg_match('/\$[^$]+\$|\$\$[\s\S]+?\$\$|\\\\\([\s\S]+?\\\\\)|\\\\\[[\s\S]+?\\\\\]/', $html) === 1;
    }

    /**
     * 调用 Node.js KaTeX 渲染脚本
     */
    private function callNodeScript(string $html): string
    {
        // 检查脚本是否存在
        if (!file_exists($this->scriptPath)) {
            Log::warning('KatexRenderer: 渲染脚本不存在', ['path' => $this->scriptPath]);
            return $html;
        }

        $configuredBinary = trim((string) config('math-render.katex.node_binary', 'node'));
        $candidates = array_values(array_unique(array_filter([
            $configuredBinary ?: 'node',
            'node', // 兜底，避免误配时直接失败
        ])));

        $lastError = null;
        $lastExitCode = null;

        foreach ($candidates as $nodeBinary) {
            try {
                $process = new Process([$nodeBinary, $this->scriptPath]);
                $process->setInput($html);
                $process->setTimeout(30); // 30秒超时
                $process->run();

                if (!$process->isSuccessful()) {
                    $lastExitCode = $process->getExitCode();
                    $lastError = trim($process->getErrorOutput()) ?: trim($process->getOutput());
                    Log::warning('KatexRenderer: Node.js 脚本执行失败，尝试下一个候选', [
                        'node_binary' => $nodeBinary,
                        'exit_code' => $lastExitCode,
                        'error' => $lastError,
                    ]);
                    continue;
                }

                $output = $process->getOutput();
                if (empty($output)) {
                    Log::warning('KatexRenderer: Node.js 脚本输出为空', [
                        'node_binary' => $nodeBinary,
                    ]);
                    return $html;
                }

                Log::info('KatexRenderer: LaTeX 公式渲染成功', [
                    'node_binary' => $nodeBinary,
                    'input_length' => strlen($html),
                    'output_length' => strlen($output),
                ]);

                return $output;
            } catch (\Exception $e) {
                $lastError = $e->getMessage();
                Log::warning('KatexRenderer: 渲染异常，尝试下一个候选', [
                    'node_binary' => $nodeBinary,
                    'error' => $lastError,
                ]);
            }
        }

        Log::error('KatexRenderer: 所有Node候选均执行失败', [
            'node_candidates' => $candidates,
            'last_exit_code' => $lastExitCode,
            'last_error' => $lastError,
        ]);

        return $html;
    }

    /**
     * 生成缓存键
     */
    private function getCacheKey(string $html): string
    {
        return self::CACHE_PREFIX . md5($html);
    }

    /**
     * 禁用缓存（用于调试）
     */
    public function disableCache(): self
    {
        $this->cacheEnabled = false;
        return $this;
    }

    /**
     * 启用缓存
     */
    public function enableCache(): self
    {
        $this->cacheEnabled = true;
        return $this;
    }

    /**
     * 清除所有 KaTeX 渲染缓存
     */
    public function clearCache(): void
    {
        // 注意：这个方法需要 Redis 或支持通配符删除的缓存驱动
        Log::info('KatexRenderer: 缓存清除请求（需要手动清理或使用 Redis）');
    }

    private function sanitizeLatexInHtml(string $html): string
    {
        $sanitize = function (string $tex): string {
            $decoded = html_entity_decode($tex, ENT_QUOTES, 'UTF-8');
            while ($decoded !== $tex) {
                $tex = $decoded;
                $decoded = html_entity_decode($tex, ENT_QUOTES, 'UTF-8');
            }

            // 清理公式内部的换行与 <br>，避免 \frac{M}\n{N} 破坏解析
            $tex = preg_replace('/<br\\s*\\/?>/i', '', $tex);
            $tex = preg_replace('/\\r\\n|\\r|\\n/', '', $tex);

            // 处理 KaTeX 不支持的操作符命令
            $tex = preg_replace('/\\\\Arg\\b/', '\\\\operatorname{Arg}', $tex);
            // 修复漏空格的 \quad/\qquad（如 \quadz、\quadx）
            $tex = preg_replace('/\\\\q(u)?ad(?=[A-Za-z0-9])/', '\\\\q$1ad ', $tex);

            return $this->fixCasesLineBreaks($tex);
        };

        // $$...$$
        $html = preg_replace_callback('/\$\$([\s\S]*?)\$\$/', function ($m) use ($sanitize) {
            return '$$' . $sanitize($m[1]) . '$$';
        }, $html);

        // $...$ (avoid $$)
        $html = preg_replace_callback('/(?<!\$)\$([^$\n]+?)\$(?!\$)/', function ($m) use ($sanitize) {
            return '$' . $sanitize($m[1]) . '$';
        }, $html);

        // \(...\)
        $html = preg_replace_callback('/\\\\\(([\s\S]*?)\\\\\)/', function ($m) use ($sanitize) {
            return '\\(' . $sanitize($m[1]) . '\\)';
        }, $html);

        // \[...\]
        $html = preg_replace_callback('/\\\\\[([\s\S]*?)\\\\\]/', function ($m) use ($sanitize) {
            return '\\[' . $sanitize($m[1]) . '\\]';
        }, $html);

        return $html;
    }

    private function fixCasesLineBreaks(string $tex): string
    {
        return preg_replace_callback('/\\\\begin\{cases\}([\s\S]*?)\\\\end\{cases\}/', function ($m) {
            $content = $m[1];
            // 将 cases 中被转成单反斜杠的换行恢复为双反斜杠（仅处理紧跟 +/- 的情况）
            $content = preg_replace('/(?<!\\\\)\\\\(?=[-+])/', '\\\\\\\\', $content);
            // 行首是变量/数字的情况（如 \\3x 或 \\a_1x 或 \\x=...）
            $content = preg_replace('/(?<!\\\\)\\\\(?=[0-9])/', '\\\\\\\\', $content);
            $content = preg_replace('/(?<!\\\\)\\\\(?=[A-Za-z](?:[_^0-9=<>]|\\s))/', '\\\\\\\\', $content);
            return '\\begin{cases}' . $content . '\\end{cases}';
        }, $tex);
    }

    private function extractKatexErrorSnippet(string $html): array
    {
        if (!preg_match('/<span class="katex-error"[^>]*>(.*?)<\/span>/is', $html, $match)) {
            return [];
        }

        $text = trim(strip_tags($match[1]));
        $text = preg_replace('/\s+/', ' ', $text);

        return [
            'text' => mb_substr($text, 0, 200),
        ];
    }
}
