<?php

namespace XZD\AI\V5\Tools;

use XZD\AI\V5\AI\Client;
use XZD\AI\V5\AI\Types;
use XZD\AI\V5\Core\Logger;
use XZD\AI\V5\Core\PreserveDates;
use XZD\AI\V5\Theme\AdapterRegistry;

/**
 * 内容工具管理器 — 12个内容创作工具
 *
 * 标签/SEO/摘要/润色/翻译/改写/扩展/封面/ALT/内链/质量/去重
 */
class ContentManager
{
    /**
     * 恢复备份 — 将文章内容恢复为AI修改前的原文
     */
    public static function restoreBackup(array $params, int $userId): ToolResult
    {
        $postId = (int) ($params['post_id'] ?? 0);
        $post = get_post($postId);
        if (!$post) return ToolResult::error("文章不存在: {$postId}");

        $backup = get_post_meta($postId, '_xzd_ai_content_backup', true);
        if (empty($backup)) {
            return ToolResult::error('没有可用的备份内容');
        }

        PreserveDates::updatePost([
            'ID'           => $postId,
            'post_content' => $backup,
        ]);

        // 清除AI处理标记，允许重新处理
        delete_post_meta($postId, '_xzd_ai_polished');
        delete_post_meta($postId, '_xzd_ai_rewritten');
        delete_post_meta($postId, '_xzd_ai_expanded');
        delete_post_meta($postId, '_xzd_ai_autolinks');
        delete_post_meta($postId, '_xzd_ai_content_backup');

        return ToolResult::success([], '原文已恢复，AI处理标记已清除。请刷新页面查看。');
    }

    /**
     * 自动备份文章内容（仅首次备份，保留原始版本）
     */
    private static function backupPostContent(int $postId): void
    {
        $existing = get_post_meta($postId, '_xzd_ai_content_backup', true);
        if (!empty($existing)) return; // 已有备份，不重复

        $post = get_post($postId);
        if ($post && !empty(trim($post->post_content))) {
            update_post_meta($postId, '_xzd_ai_content_backup', $post->post_content);
        }
    }

    /**
     * 保护HTML敏感元素：提取代码块/图片/iframe，替换为占位符
     * 返回 ['content' => 替换后的内容, 'blocks' => [占位符 => 原始HTML]]
     */
    private static function protectBlocks(string $html): array
    {
        $blocks = [];
        $i = 0;

        // 保护 <pre><code class="...">...</code></pre> 代码块（含语法高亮class）
        $html = preg_replace_callback(
            '/<pre\b[^>]*><code\b[^>]*>.*?<\/code><\/pre>/s',
            function ($m) use (&$blocks, &$i) {
                $key = "%%CODEBLOCK_{$i}%%";
                $blocks[$key] = $m[0];
                $i++;
                return $key;
            },
            $html
        );

        // 保护 Zibll enlighter 代码高亮块
        $html = preg_replace_callback(
            '/<!--\s*wp:zibllblock\/enlighter\b.*?\/wp:zibllblock\/enlighter\s*-->/s',
            function ($m) use (&$blocks, &$i) {
                $key = "%%CODEBLOCK_{$i}%%";
                $blocks[$key] = $m[0];
                $i++;
                return $key;
            },
            $html
        );

        // 保护非文本 Gutenberg 块（代码/表格/短代码等，避免AI破坏结构）
        $html = preg_replace_callback(
            '/<!--\s*wp:([a-z]+\/[a-z]+)\b.*?\/wp:\1\s*-->/s',
            function ($m) use (&$blocks, &$i) {
                // 跳过文本类块，让AI处理
                $skipTypes = ['core/paragraph', 'core/heading', 'core/list', 'core/list-item', 'core/quote'];
                if (in_array($m[1], $skipTypes)) {
                    return $m[0];
                }
                $key = "%%WPBLOCK_{$i}%%";
                $blocks[$key] = $m[0];
                $i++;
                return $key;
            },
            $html
        );

        // 保护 <a ...>...</a> 链接标签（防止润色时内链丢失）
        $html = preg_replace_callback(
            '/<a\b[^>]*>.*?<\/a>/is',
            function ($m) use (&$blocks, &$i) {
                $key = "%%LINK_{$i}%%";
                $blocks[$key] = $m[0];
                $i++;
                return $key;
            },
            $html
        );

        // 保护 <img ...> 标签
        $html = preg_replace_callback(
            '/<img\b[^>]*\/?>/i',
            function ($m) use (&$blocks, &$i) {
                $key = "%%IMAGE_{$i}%%";
                $blocks[$key] = $m[0];
                $i++;
                return $key;
            },
            $html
        );

        // 保护 <iframe ...></iframe>
        $html = preg_replace_callback(
            '/<iframe\b[^>]*>.*?<\/iframe>/s',
            function ($m) use (&$blocks, &$i) {
                $key = "%%IFRAME_{$i}%%";
                $blocks[$key] = $m[0];
                $i++;
                return $key;
            },
            $html
        );

        // V3.1.7: 保护内联格式标签 — 转为Markdown防止AI润色时丢失粗体/斜体等样式
        // <strong>/<b> → **, <em>/<i> → *, <u> → __, <del>/<s> → ~~
        $inlineTags = [
            '/<strong\b[^>]*>(.*?)<\/strong>/is' => '**$1**',
            '/<b\b[^>]*>(.*?)<\/b>/is'           => '**$1**',
            '/<em\b[^>]*>(.*?)<\/em>/is'         => '*$1*',
            '/<i\b[^>]*>(.*?)<\/i>/is'           => '*$1*',
            '/<u\b[^>]*>(.*?)<\/u>/is'           => '__$1__',
            '/<del\b[^>]*>(.*?)<\/del>/is'       => '~~$1~~',
            '/<s\b[^>]*>(.*?)<\/s>/is'           => '~~$1~~',
        ];
        foreach ($inlineTags as $pattern => $replacement) {
            $html = preg_replace($pattern, $replacement, $html);
        }

        return ['content' => $html, 'blocks' => $blocks];
    }

    /**
     * 还原被保护的HTML敏感元素
     */
    private static function restoreBlocks(string $html, array $blocks): string
    {
        foreach ($blocks as $key => $original) {
            // 忽略大小写和可能被AI添加的空格
            $html = str_replace($key, $original, $html);
            $html = str_replace(str_replace('%%', '%% ', $key), $original, $html);
        }
        return $html;
    }

    /**
     * ★ 文章归属检查 — 管理员跳过，普通用户只允许操作自己的文章
     */
    private static function checkPostOwnership(int $postId, int $userId): ?ToolResult
    {
        $post = get_post($postId);
        if (!$post) {
            return ToolResult::error("文章不存在: {$postId}");
        }
        // 管理员跳过
        if ($userId && current_user_can('manage_options')) {
            return null;
        }
        // 普通用户只能操作自己的文章
        if ((int) $post->post_author !== $userId) {
            return ToolResult::error('只能操作自己发布的文章', 'forbidden');
        }
        return null;
    }

    /**
     * 生成标签 (AI模式)
     */
    public static function generateTags(array $params, int $userId): ToolResult
    {
        $postId = (int) ($params['post_id'] ?? 0);
        $count = (int) ($params['count'] ?? 5);
        $mode = $params['mode'] ?? 'ai';
        $procMode = $params['_processing_mode'] ?? 'fill_empty';

        $post = get_post($postId);
        if (!$post) {
            return ToolResult::error("文章不存在: {$postId}");
        }

        // 覆盖模式：先清除文章所有现有标签
        $existingNames = [];
        if ($procMode === 'overwrite') {
            $existing = wp_get_post_tags($postId, ['fields' => 'names']);
            wp_set_post_tags($postId, []);
        } elseif ($procMode === 'append') {
            $existing = wp_get_post_tags($postId, ['fields' => 'names']);
            $existingNames = is_array($existing) ? $existing : [];
        }

        $content = wp_strip_all_tags(strip_shortcodes($post->post_content));
        $content = mb_substr($content, 0, 3000);

        // ═══ 模式分发：library / hybrid / ai ═══
        // library：纯词库匹配，匹配不到就返回空
        // hybrid：先词库匹配 → 不足 $count 个用 AI 补齐
        // ai：纯 AI 生成
        $libraryTags = [];
        if ($mode === 'library' || $mode === 'hybrid') {
            $libraryTags = self::matchFromLibraryRaw($content, $postId, $count);
            // 写入词库匹配到的标签
            foreach ($libraryTags as $tag) {
                wp_set_post_tags($postId, [$tag['name']], true);
            }
            if ($mode === 'library') {
                // 纯词库模式：直接返回匹配结果
                update_post_meta($postId, '_xzd_ai_tags_generated', current_time('mysql'));
                update_post_meta($postId, '_xzd_ai_tags_count', count($libraryTags));
                update_post_meta($postId, '_xzd_ai_tags_list', array_column($libraryTags, 'name'));
                return ToolResult::success(
                    ['tags' => $libraryTags, 'count' => count($libraryTags), 'skipped' => 0, 'mode' => $procMode, 'source' => 'library'],
                    count($libraryTags) > 0
                        ? "词库匹配 " . count($libraryTags) . " 个标签"
                        : '词库未匹配到标签（标签库为空或内容无匹配词）'
                );
            }
            // hybrid 模式继续往下走 AI 补齐
        }

        // ═══ AI 生成（ai 模式 或 hybrid 模式补齐） ═══
        $aiNeeded = $mode === 'hybrid' ? max(0, $count - count($libraryTags)) : $count;
        $libraryNames = array_column($libraryTags, 'name');
        $created = [];
        $skipped = 0;
        $aiResult = null; // 用于 tokens_used

        if ($aiNeeded > 0) {
            $prompt = "为以下文章内容生成 {$aiNeeded} 个相关标签。返回 JSON 数组，每个标签包含 name 和 confidence(0-100)：\n\n{$content}";
            // 混合模式：告知 AI 已有标签，避免重复
            if ($mode === 'hybrid' && !empty($libraryNames)) {
                $prompt .= "\n\n已有标签（请勿重复生成）：" . implode('、', $libraryNames);
            }

            // V16：防御 AI 客户端内部异常，失败时降级到普通 chat
            $aiResult = null;
            try {
                $aiResult = Client::instance()->chatStructured(
                    [Types::system('你是标签专家，返回 JSON 数组。'), Types::user($prompt)],
                    [
                        'type' => 'object',
                        'properties' => [
                            'tags' => [
                                'type' => 'array',
                                'items' => [
                                    'type' => 'object',
                                    'properties' => [
                                        'name' => ['type' => 'string'],
                                        'confidence' => ['type' => 'integer'],
                                    ],
                                    'required' => ['name', 'confidence'],
                                ],
                            ],
                        ],
                        'required' => ['tags'],
                    ],
                    ['max_tokens' => 500]
                );
            } catch (\Throwable $e) {
                Logger::warning('generate_tags', 'AI结构化调用异常，降级到普通chat: ' . $e->getMessage(), [
                    'post_id' => $postId,
                ]);
                try {
                    $fallbackPrompt = $prompt . "\n\n请只输出 JSON：{\"tags\":[{\"name\":\"标签名\",\"confidence\":80}]}";
                    $rawResult = Client::instance()->chat(
                        [Types::system('你是标签专家，返回 JSON 数组。'), Types::user($fallbackPrompt)],
                        ['max_tokens' => 500]
                    );
                    if (!isset($rawResult['error'])) {
                        $text = $rawResult['content'] ?? '';
                        $parsed = null;
                        if (preg_match('/```(?:json)?\s*(\{[\s\S]*?\})\s*```/', $text, $m)) {
                            $parsed = json_decode($m[1], true);
                        }
                        if (!$parsed) {
                            $parsed = json_decode($text, true);
                        }
                        if ($parsed && isset($parsed['tags'])) {
                            $aiResult = array_merge($rawResult, ['content' => $parsed]);
                        }
                    }
                } catch (\Throwable $e2) {
                    Logger::error('generate_tags', '降级chat也失败: ' . $e2->getMessage());
                }
                if (!$aiResult) {
                    // AI 完全失败但有词库标签 → 返回词库结果
                    if (!empty($libraryTags)) {
                        return ToolResult::success(
                            ['tags' => $libraryTags, 'count' => count($libraryTags), 'skipped' => 0, 'mode' => $procMode, 'source' => 'library_fallback'],
                            'AI 调用失败，回退使用词库匹配 ' . count($libraryTags) . ' 个标签'
                        );
                    }
                    return ToolResult::error('AI 调用异常：' . $e->getMessage());
                }
            }

            if (isset($aiResult['error'])) {
                if (!empty($libraryTags)) {
                    return ToolResult::success(
                        ['tags' => $libraryTags, 'count' => count($libraryTags), 'skipped' => 0, 'mode' => $procMode, 'source' => 'library_fallback'],
                        'AI 返回错误，回退使用词库匹配 ' . count($libraryTags) . ' 个标签：' . $aiResult['error']
                    );
                }
                return ToolResult::error($aiResult['error']);
            }

            // 解析 AI 返回
            $parsedContent = $aiResult['content'] ?? [];
            if (!is_array($parsedContent)) {
                Logger::warning('generate_tags', 'AI 返回非结构化数据，无法解析标签', [
                    'post_id' => $postId, 'type' => gettype($parsedContent),
                ]);
                if (!empty($libraryTags)) {
                    return ToolResult::success(
                        ['tags' => $libraryTags, 'count' => count($libraryTags), 'skipped' => 0, 'mode' => $procMode, 'source' => 'library_fallback'],
                        'AI 返回格式异常，回退使用词库匹配 ' . count($libraryTags) . ' 个标签'
                    );
                }
                return ToolResult::error('AI 返回格式异常，无法解析标签数据');
            }

            $aiTags = $parsedContent['tags'] ?? [];

            // ★ 关键修复：按置信度排序，截断到 aiNeeded 个
            usort($aiTags, fn($a, $b) => ($b['confidence'] ?? 0) <=> ($a['confidence'] ?? 0));
            $aiTags = array_slice($aiTags, 0, $aiNeeded);

            // 写入 AI 标签
            foreach ($aiTags as $tag) {
                if (($tag['confidence'] ?? 0) < ($params['min_confidence'] ?? 60)) continue;
                // 去重：跳过已存在于词库匹配或手动标签中的
                if (in_array($tag['name'], $libraryNames, true)) continue;
                if ($procMode === 'append' && in_array($tag['name'], $existingNames, true)) {
                    $skipped++;
                    continue;
                }
                $term = term_exists($tag['name'], 'post_tag');
                if (!$term) {
                    $term = wp_insert_term($tag['name'], 'post_tag');
                }
                if (!is_wp_error($term)) {
                    $tid = is_array($term) ? $term['term_id'] : (int) $term;
                    $created[] = ['id' => $tid, 'name' => $tag['name'], 'confidence' => $tag['confidence']];
                    wp_set_post_tags($postId, [$tag['name']], true);
                }
            }
        }

        // 合并词库 + AI 结果
        $allCreated = array_merge($libraryTags, $created);

        // 更新文章标签索引
        update_post_meta($postId, '_xzd_ai_tags_generated', current_time('mysql'));
        update_post_meta($postId, '_xzd_ai_tags_count', count($allCreated));
        $aiTagNames = array_column($allCreated, 'name');
        if ($procMode === 'append') {
            $prevAI = get_post_meta($postId, '_xzd_ai_tags_list', true) ?: [];
            $aiTagNames = array_unique(array_merge($prevAI, $aiTagNames));
        }
        update_post_meta($postId, '_xzd_ai_tags_list', $aiTagNames);

        $libCount = count($libraryTags);
        $aiCount = count($created);
        $total = count($allCreated);
        $source = $mode === 'hybrid' ? 'hybrid' : 'ai';
        $msgParts = [];
        if ($libCount > 0) $msgParts[] = "词库匹配 {$libCount} 个";
        if ($aiCount > 0) $msgParts[] = "AI生成 {$aiCount} 个";
        $msg = "成功生成 {$total} 个标签（" . implode(' + ', $msgParts) . "）";
        if ($procMode === 'overwrite' && !empty($existing)) {
            $msg .= "（已清除原有 " . count($existing) . " 个标签）";
        } elseif ($procMode === 'append' && $skipped > 0) {
            $msg .= "（跳过 {$skipped} 个已存在的标签）";
        }

        return ToolResult::success(
            ['tags' => $allCreated, 'count' => $total, 'library_count' => $libCount, 'ai_count' => $aiCount,
             'skipped' => $skipped, 'mode' => $procMode, 'source' => $source],
            $msg,
            ['undo_data' => ['post_id' => $postId, 'tags' => array_column($allCreated, 'name')],
             'tokens_used' => $aiResult ? ($aiResult['input_tokens'] ?? 0) + ($aiResult['output_tokens'] ?? 0) : 0]
        );
    }

    /**
     * 生成 SEO 元数据（标题+关键词+描述）
     */
    public static function generateSEO(array $params, int $userId): ToolResult
    {
        $postId = (int) ($params['post_id'] ?? 0);
        $procMode = $params['_processing_mode'] ?? 'fill_empty';
        $post = get_post($postId);
        if (!$post) return ToolResult::error("文章不存在: {$postId}");

        $content = wp_strip_all_tags(strip_shortcodes($post->post_content));
        $content = mb_substr($content, 0, 3000);

        $prompt = "为以下文章生成 SEO 元数据。返回 JSON：\n标题：40-70字\n关键词：逗号分隔\n描述：120-160字\n\n文章：{$post->post_title}\n{$content}";

        // V16：防御 AI 客户端内部异常（加密 Client.php 可能在结构化解析时崩溃）
        //   先尝试结构化调用，失败时降级到普通 chat + 手动 JSON 解析
        $result = null;
        try {
            $result = Client::instance()->chatStructured(
                [Types::user($prompt)],
                [
                    'type' => 'object',
                    'properties' => [
                        'seo_title' => ['type' => 'string'],
                        'seo_keywords' => ['type' => 'string'],
                        'seo_description' => ['type' => 'string'],
                    ],
                    'required' => ['seo_title', 'seo_keywords', 'seo_description'],
                ],
                ['max_tokens' => 500]
            );
        } catch (\Throwable $e) {
            Logger::warning('generate_seo', 'AI结构化调用异常，降级到普通chat: ' . $e->getMessage(), [
                'post_id' => $postId,
            ]);
            // 降级：用普通 chat 请求，手动解析 JSON
            try {
                $fallbackPrompt = $prompt . "\n\n请只输出 JSON 对象，格式：{\"seo_title\":\"...\",\"seo_keywords\":\"...\",\"seo_description\":\"...\"}";
                $rawResult = Client::instance()->chat(
                    [Types::user($fallbackPrompt)],
                    ['max_tokens' => 500]
                );
                if (!isset($rawResult['error'])) {
                    $text = $rawResult['content'] ?? '';
                    // 尝试提取 JSON
                    $parsed = null;
                    if (preg_match('/```(?:json)?\s*(\{[\s\S]*?\})\s*```/', $text, $m)) {
                        $parsed = json_decode($m[1], true);
                    }
                    if (!$parsed) {
                        $parsed = json_decode($text, true);
                    }
                    if ($parsed && isset($parsed['seo_title'])) {
                        $result = array_merge($rawResult, ['content' => $parsed]);
                    }
                }
            } catch (\Throwable $e2) {
                Logger::error('generate_seo', '降级chat也失败: ' . $e2->getMessage());
            }
            if (!$result) {
                return ToolResult::error('AI 调用异常：' . $e->getMessage());
            }
        }

        if (isset($result['error'])) return ToolResult::error($result['error']);

        // V16：防御 AI 返回非数组格式
        $seo = $result['content'];
        if (!is_array($seo)) {
            Logger::warning('generate_seo', 'AI 返回非结构化数据，无法解析SEO', [
                'post_id' => $postId,
                'type'    => gettype($seo),
            ]);
            return ToolResult::error('AI 返回格式异常，无法解析SEO数据');
        }

        // 追加模式：合并关键词（去重），拼接描述，保留原标题
        if ($procMode === 'append') {
            $existingTitle = get_post_meta($postId, '_xzd_ai_seo_title', true);
            $existingKeywords = get_post_meta($postId, '_xzd_ai_seo_keywords', true);
            $existingDesc = get_post_meta($postId, '_xzd_ai_seo_description', true);

            if ($existingKeywords) {
                $oldKws = array_map('trim', explode(',', $existingKeywords));
                $newKws = array_map('trim', explode(',', $seo['seo_keywords'] ?? ''));
                $merged = array_unique(array_merge($newKws, $oldKws));
                $seo['seo_keywords'] = implode(',', $merged);
            }
            // 标题优先保留已存在的，新标题作为备选
            if ($existingTitle && $existingTitle !== $seo['seo_title']) {
                $seo['seo_title_alt'] = $seo['seo_title'];
                $seo['seo_title'] = $existingTitle;
            }
            // 描述：拼接新描述到已有描述之后
            if ($existingDesc && $existingDesc !== $seo['seo_description']) {
                $seo['seo_description'] = $existingDesc . "\n" . $seo['seo_description'];
            }
            $msg = 'SEO 数据追加完成（关键词已合并去重）';
        } else {
            $msg = 'SEO 数据生成成功（覆盖原有）';
        }

        // 通道1：写入插件自有追踪键
        update_post_meta($postId, '_xzd_ai_seo_title', $seo['seo_title'] ?? '');
        update_post_meta($postId, '_xzd_ai_seo_keywords', $seo['seo_keywords'] ?? '');
        update_post_meta($postId, '_xzd_ai_seo_description', $seo['seo_description'] ?? '');
        update_post_meta($postId, '_xzd_ai_seo_generated', current_time('mysql'));

        // 通道2：通过主题适配器写入主题原生SEO字段
        $adapter = AdapterRegistry::getCurrent();
        $adapter->setSEOTitle($postId, $seo['seo_title'] ?? '');
        $adapter->setSEODescription($postId, $seo['seo_description'] ?? '');
        $adapter->setSEOKeywords($postId, array_map('trim', explode(',', $seo['seo_keywords'] ?? '')));

        return ToolResult::success($seo, $msg, [
            'tokens_used' => $result['input_tokens'] + $result['output_tokens'],
        ]);
    }

    /**
     * 生成摘要
     */
    public static function generateSummary(array $params, int $userId): ToolResult
    {
        $postId = (int) ($params['post_id'] ?? 0);
        $length = (int) ($params['length'] ?? 100);
        $post = get_post($postId);
        if (!$post) return ToolResult::error("文章不存在: {$postId}");

        $content = mb_substr(wp_strip_all_tags(strip_shortcodes($post->post_content)), 0, 5000);

        // V16：防御 AI 客户端内部异常
        try {
            $result = Client::instance()->chat(
                [Types::user("为以下文章生成 {$length} 字以内的中文摘要，直接输出摘要：\n\n{$content}")],
                ['max_tokens' => 300]
            );
        } catch (\Throwable $e) {
            Logger::warning('generate_summary', 'AI客户端调用异常: ' . $e->getMessage(), [
                'post_id' => $postId,
            ]);
            return ToolResult::error('AI 调用异常：' . $e->getMessage());
        }

        if (isset($result['error'])) return ToolResult::error($result['error']);

        $summary = trim($result['content']);
        $procMode = $params['_processing_mode'] ?? 'fill_empty';

        if ($procMode === 'append') {
            $existingSummary = get_post_meta($postId, '_xzd_ai_summary', true);
            if ($existingSummary && $existingSummary !== $summary) {
                $summary = $existingSummary . "\n\n" . $summary;
            }
            $msg = '摘要追加完成';
        } else {
            $msg = '摘要生成成功（覆盖原有）';
        }
        update_post_meta($postId, '_xzd_ai_summary', $summary);
        update_post_meta($postId, '_xzd_ai_summary_generated', current_time('mysql'));

        return ToolResult::success(['summary' => $summary], $msg, [
            'tokens_used' => $result['input_tokens'] + $result['output_tokens'],
        ]);
    }

    /**
     * 润色文章内容 — 保留Gutenberg块结构
     */
    public static function polishContent(array $params, int $userId): ToolResult
    {
        $postId = (int) ($params['post_id'] ?? 0);
        $post = get_post($postId);
        if (!$post) return ToolResult::error("文章不存在: {$postId}");

        self::backupPostContent($postId);

        $depth = $params['depth'] ?? 'medium';

        // ═══ links_only 模式：仅优化内链融入度 ═══
        if ($depth === 'links_only') {
            return self::polishLinksOnly($post, $postId, $userId);
        }

        // ═══ 检测待融入的内链标记 ═══
        $content = $post->post_content;
        $hasUnmatchedLinks = (strpos($content, '<!-- xzd-autolink-unmatched -->') !== false);
        $linkCountBefore = $hasUnmatchedLinks ? substr_count($content, 'xzd-tag-link') : 0;
        $imgCountBefore = substr_count($content, '<img');  // V3.1.7修复：追踪图片数量防止AI润色丢图

        // ═══ 构建系统提示词（内链融入 + 分级润色 + FAQ格式） ═══
        $systemPrompt = self::buildPolishSystemPrompt($depth, $hasUnmatchedLinks, $linkCountBefore);

        // ═══ 构建用户消息（全文HTML） ═══
        $userMessage = "请对以下文章进行润色，直接返回完整HTML内容。\n\n"
            . "=== 文章标题 ===\n{$post->post_title}\n\n"
            . "=== 文章内容 ===\n{$content}";

        // ═══ V3.2：全文一次AI调用 ═══
        $maxRetries = 3;
        $lastError = null;
        $totalTokens = 0;

        for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
            $aiResult = Client::instance()->chat(
                [
                    Types::system($systemPrompt),
                    Types::user($userMessage),
                ],
                ['max_tokens' => 16000]
            );

            $totalTokens += ($aiResult['input_tokens'] ?? 0) + ($aiResult['output_tokens'] ?? 0);

            if (isset($aiResult['error'])) {
                $lastError = $aiResult['error'];
                Logger::info('polish', "润色第{$attempt}次AI调用失败: {$lastError}");
                continue;
            }

            $polished = trim($aiResult['content']);
            // 清理可能的markdown包裹
            $polished = preg_replace('/^```[a-z]*\s*/i', '', $polished);
            $polished = preg_replace('/\s*```$/i', '', $polished);

            // ═══ 验证润色结果 ═══
            $validationError = self::validatePolishContent($polished, $content, $linkCountBefore, $imgCountBefore);
            if ($validationError) {
                Logger::info('polish', "润色第{$attempt}次验证失败: {$validationError}");
                $lastError = $validationError;
                continue;
            }

            // ═══ 成功：清理标记块，写回文章 ═══
            if ($hasUnmatchedLinks) {
                $polished = preg_replace(
                    '/\n*<!-- xzd-autolink-unmatched -->.*?<!-- \/xzd-autolink-unmatched -->\n*/s',
                    '', $polished
                );
            }

            // 备份原文
            if (!get_post_meta($postId, '_xzd_ai_original', true)) {
                update_post_meta($postId, '_xzd_ai_original', $post->post_content);
            }

            update_post_meta($postId, '_xzd_ai_polished', $polished);
            update_post_meta($postId, '_xzd_ai_polish_level', $depth);

            PreserveDates::updatePost([
                'ID'           => $postId,
                'post_content' => $polished,
            ]);

            $msg = $hasUnmatchedLinks ? '润色完成，内链已丝滑融入正文' : '润色完成';
            return ToolResult::success(['depth' => $depth], $msg, [
                'tokens_used' => $totalTokens,
                'undo_data'   => ['post_id' => $postId],
            ]);
        }

        // ═══ 3次全部失败 → 降级：恢复原文 + 正则内链 ═══
        Logger::warning('polish', "文章{$postId}润色{$maxRetries}次全部失败，降级为原文+正则内链。最后错误: {$lastError}");

        // 恢复原文
        $fallbackContent = $post->post_content;
        if ($hasUnmatchedLinks) {
            // 用PHP正则方式将unmatched链接注入正文
            $fallbackContent = self::fallbackInsertUnmatchedLinks($fallbackContent, $postId);
            $fallbackContent = preg_replace(
                '/\n*<!-- xzd-autolink-unmatched -->.*?<!-- \/xzd-autolink-unmatched -->\n*/s',
                '', $fallbackContent
            );
        }

        PreserveDates::updatePost([
            'ID'           => $postId,
            'post_content' => $fallbackContent,
        ]);

        return ToolResult::success(
            ['depth' => $depth, 'fallback' => true],
            $hasUnmatchedLinks ? '润色失败，已降级：原文保留，内链已正则注入' : '润色失败，原文已保留',
            ['tokens_used' => $totalTokens, 'undo_data' => ['post_id' => $postId]]
        );
    }

    // ═══════════════════════════════════════════════════════════════════════
    // V3.1.8：润色系统提示词构建器 — 参考旧版 class-engine-polish.php
        // ═══════════════════════════════════════════════════════════════════════
    // V3.2：润色系统提示词构建器 — 参考旧版 class-engine-polish.php 成熟方案
    
// ═══════════════════════════════════════════════════════════════════════
    // V3.2：润色系统提示词构建器 — 参考旧版 class-engine-polish.php 成熟方案
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * 构建润色系统提示词（作为 system role 发送给AI）
     */
    private static function buildPolishSystemPrompt(string $depth, bool $hasLinks, int $linkCount): string
    {
        $prompt = '你是资深内容编辑+SEO专家+排版设计师的合体。';

        // 内链融入（最前面，所有力度必须首要执行）
        if ($hasLinks) {
            $prompt .= "\n\n【内链融入 · 第一优先级 · 先于一切规则执行】\n"
                . "文章末尾有 <!-- xzd-autolink-unmatched --> 标记块，其中包含SEO内链标签。\n"
                . "全文共有{$linkCount}个 xzd-tag-link。\n\n"
                . "你的首要任务——在动笔润色之前先规划好：\n"
                . "1. 先找到<!-- xzd-autolink-unmatched -->标记块，读清里面有几个内链\n"
                . "2. 在正文中找到每个关键词语义最匹配的位置（分散到不同段落，不得集中堆砌）\n"
                . "3. 将该关键词包裹为<a>标签格式，href/class/style完全不动\n"
                . "4. 如果定位后句子生硬，必须扩写1-2句话让上下文丝滑包裹链接\n"
                . "5. 删除<!-- xzd-autolink-unmatched -->和<!-- /xzd-autolink-unmatched -->注释标记及「相关阅读」段落\n\n"
                . "⚠️ 这是硬性指标。润色完成后必须自己数一遍xzd-tag-link出现次数不少于{$linkCount}。\n"
                . "⚠️ 任何时候不要为了FAQ/排版/去AI味而牺牲内链——内链是第一优先级。";
        }

        // 三级力度
        switch ($depth) {
            case 'light':
                $prompt .= "\n\n【轻微润色】\n"
                    . "- 修正错别字、标点错误、语法问题\n"
                    . "- 不改结构、不改排版、不删除内容\n"
                    . "- 不改任何HTML标签和属性（特别是img标签，一个都不能少）\n"
                    . "- 保持原文作者语气和口吻，不要变得像AI生成的\n"
                    . ($hasLinks ? "- 内链已按上文要求融入，保持不动\n" : '');
                break;

            case 'deep':
                $prompt .= "\n\n【深度润色】你是WordPress首席内容设计师，有最大自主权。目标：让文章读起来像专业编辑精心排版的成果。\n\n"
                    . "## 结构重塑\n"
                    . "1. 开头直接切入正题，用有吸引力的hook句（疑问/数据/场景/反常识）开场\n"
                    . "2. 根据全文逻辑重建H2/H3层级，H2尽量改为疑问句式增强点击欲\n"
                    . "3. 消灭「文字墙」：连续4句以上纯文字无视觉元素必须重组\n\n"
                    . "## WP编辑器格式（按内容类型灵活选用）\n"
                    . "步骤/流程/教程 → ol/li 有序列表\n"
                    . "要点/特性/优势 → ul/li 无序列表\n"
                    . "重要观点/核心结论 → blockquote 引用块\n"
                    . "对比/对照/参数 → table 表格\n"
                    . "代码/命令/配置 → pre 或 code\n"
                    . "补充说明/扩展材料 → details/summary 折叠块\n"
                    . "警示/注意/提示 → 粗体小标题+缩进段落\n"
                    . "关键术语首次出现 → strong 加粗\n\n"
                    . "## 结构化内容自动美化 · 强制执行\n"
                    . "检测到以下类型数据时，必须使用指定格式。如文章中没有该类型则跳过。\n\n"
                    . "【软件/资源信息】→ 资源卡片\n"
                    . "格式：\n"
                    . "<div class=\"xzd-resource-card\">\n"
                    . "  <div class=\"xzd-param\"><strong>📦 资源名称：</strong>...</div>\n"
                    . "  <div class=\"xzd-param\"><strong>📅 更新日期：</strong>...</div>\n"
                    . "  <div class=\"xzd-param\"><strong>💰 资费说明：</strong>...</div>\n"
                    . "  <div class=\"xzd-param\"><strong>🛡️ 安全说明：</strong>...</div>\n"
                    . "  <div class=\"xzd-param\"><strong>⭐ 客户评分：</strong>...</div>\n"
                    . "</div>\n"
                    . "只显示原文中实际存在的参数，没有的参数不要编造。\n\n"
                    . "【课程/教程信息】→ 课程卡片\n"
                    . "格式同上，参数名改用：课程名称/课时数量/适合人群/教学方式/课程大纲\n\n"
                    . "【硬件/设备参数】→ 规格表\n"
                    . "格式：双列table，左列参数名(灰字)，右列参数值\n\n"
                    . "【下载/安装信息】→ 下载卡片\n"
                    . "格式：版本号、文件大小、适用平台、下载链接（保留原始链接）\n\n"
                    . "【价格/套餐对比】→ 对比卡片\n"
                    . "格式：每个套餐一个div，突出推荐套餐，写明价格和包含功能\n\n"
                    . "<div class=\"xzd-price-box\">\n"
                    . "  <h4>套餐名</h4>\n"
                    . "  <div class=\"xzd-price-amount\"><strong>¥99</strong></div>\n"
                    . "  <div class=\"xzd-price-desc\">包含功能...</div>\n"
                    . "</div>\n\n"
                    . "核心原则：检测到结构化数据 → 用卡片/表格美化；纯叙述文字 → 保持段落。不强行套格式。\n"
                    . "## 常见问题结尾 · 强制执行\n"
                    . "先用H2「常见问题」（加class=\"xzd-faq-heading\"），然后紧接3-5个手风琴。绝对禁止出现FAQ这三个英文字母。\n"
                    . "严格按以下格式：\n"
                    . '<h2 class="xzd-faq-heading">常见问题</h2>' . "\n"
                    . '<details class="xzd-faq" name="faq-group">' . "\n"
                    . "  <summary>❓ 问题标题</summary>\n"
                    . '  <div class="xzd-faq-answer">答案2-4句</div>' . "\n"
                    . "</details>\n"
                    . "强制要求：所有FAQ的name统一为\"faq-group\"实现互斥展开；class只用\"xzd-faq\"和\"xzd-faq-answer\"；禁止内联style。**所有结构化内容（卡片/表格/价格对比）禁止使用内联style，统一用CSS类名，颜色自动适配日间/夜间模式。**\n\n"
                    . "## 去AI味+格式铁律\n"
                    . "严禁：值得注意的是/总而言之/首先其次最后/不仅而且/希望对你有帮助/以上就是/让我们/毋庸置疑\n"
                    . "严禁：排比句群/金句结尾/感叹呼吁。保持原文作者语气风格不变\n"
                    . "段落长短错落——有的2句有的5句，不要整齐划一\n"
                    . "H2→H3→H4不跳级，禁止H1。图片/短代码/嵌入原样保留\n"
                    . ($hasLinks ? "\n## ⚠ 内链是最高优先级\n所有内链必须融入——必要时为每个内链扩写1-2句上下文，使其丝滑嵌入。禁止遗漏任何内链。\n" : '');
                break;

            default: // medium
                $prompt .= "\n\n【标准润色】除文字修正外：\n"
                    . "- 修正错别字和语法\n"
                    . "- 超长段落(>6句)拆分为2-3段\n"
                    . "- 关键数据/结论用strong加粗\n"
                    . "- 适合列举的内容改用ul/li\n"
                    . "- 需要时可补充H3小标题\n"
                    . "- ★ 强制：所有图片/短代码/嵌入必须原样保留，禁止删除、修改、移动任何img标签\n"
                    . "- 去AI味：避免\"值得注意的是/总而言之/首先其次\"等AI套话，保持原文作者口吻和语气风格\n"
                    . ($hasLinks ? "- 内链融入是首要任务，润色时确保每个内链都在最佳位置\n" : '');
                break;
        }

        // 输出格式
        $prompt .= "\n\n【输出】直接返回完整HTML。第一个字符必须是<。不要加任何解释、说明或markdown标记。★ 所有img标签必须原样保留，数量和顺序不能变。";
        if ($hasLinks) {
            $prompt .= "\n返回前必须自检：数一遍xzd-tag-link出现次数不少于{$linkCount}。少了就是不合格。";
        }

        return $prompt;
    }

private static function validatePolishContent(string $polished, string $original, int $linkCountBefore, int $imgCountBefore = 0): ?string
    {
        // 1. 去除HTML标签后的纯文本长度对比
        $polishedText = trim(wp_strip_all_tags($polished));
        $originalText = trim(wp_strip_all_tags($original));
        $polishedLen = mb_strlen($polishedText);
        $originalLen = mb_strlen($originalText);

        // 图片完整性检测 — V3.1.7修复：AI润色可能丢弃img标签
        if ($imgCountBefore > 0) {
            $imgCountAfter = substr_count($polished, '<img');
            if ($imgCountAfter < $imgCountBefore) {
                return "图片丢失(前{$imgCountBefore}张→后{$imgCountAfter}张)，必须保留所有{$imgCountBefore}张图片";
            }
        }

        // 内容缩水检测：新文字数 < 原文35%（长文章才检测）
        if ($originalLen > 100 && $polishedLen < $originalLen * 0.35) {
            return "内容缩水严重（原{$originalLen}字→新{$polishedLen}字），可能丢失了大量内容";
        }

        // 内容过短检测
        if ($polishedLen < 30) {
            return "润色后内容过短（{$polishedLen}字），可能未正确输出";
        }

        // 无变化检测
        if ($originalLen > 50) {
            similar_text($originalText, $polishedText, $similarity);
            if ($similarity > 98) {
                return "润色前后相似度{$similarity}%，几乎无变化";
            }
        }

        // 内链完整性检测
        if ($linkCountBefore > 0) {
            $linksAfter = substr_count($polished, 'xzd-tag-link');
            if ($linksAfter < $linkCountBefore) {
                return "内链丢失(前{$linkCountBefore}→后{$linksAfter})，必须保留所有{$linkCountBefore}个内链";
            }
        }

        return null; // 验证通过
    }

    /**
     * V3.1.8: 润色失败时的内链fallback — 从unmatched标记提取链接，regex注入正文
     */
    private static function fallbackInsertUnmatchedLinks(string $content, int $postId): string
    {
        // 提取 <!-- xzd-autolink-unmatched --> 块中的所有链接
        if (!preg_match('/<!-- xzd-autolink-unmatched -->(.*?)<!-- \/xzd-autolink-unmatched -->/s', $content, $m)) {
            return $content;
        }

        $blockContent = $m[1];
        // 提取所有 xzd-tag-link 的 href 和显示文本
        if (!preg_match_all('/<a[^>]*href="([^"]*)"[^>]*class="xzd-tag-link"[^>]*>([^<]*)<\/a>/i', $blockContent, $linkMatches, PREG_SET_ORDER)) {
            // 没有链接 → 直接删除标记块
            return preg_replace('/\n*<!-- xzd-autolink-unmatched -->.*?<!-- \/xzd-autolink-unmatched -->\n*/s', '', $content);
        }

        // 去掉标记块
        $content = preg_replace('/\n*<!-- xzd-autolink-unmatched -->.*?<!-- \/xzd-autolink-unmatched -->\n*/s', '', $content);

        // 逐链接用regex插入正文
        foreach ($linkMatches as $lm) {
            $url = $lm[1];
            $keyword = trim($lm[2]);
            if (empty($keyword)) continue;

            $linkHtml = sprintf(
                '<a href="%s" class="xzd-tag-link">%s</a>',
                esc_url($url), esc_html($keyword)
            );

            $inserted = 0;
            $content = self::replaceKeywordInHTML($content, $keyword, $linkHtml, 1, $inserted);
        }

        return $content;
    }

    /**
     * links_only 润色模式 — 仅优化内链融入度
     *
     * AI只修改含有 xzd-tag-link 的句子及其紧邻上下文，
     * 将底部孤立的「相关阅读」链接分配到文章相关段落中，
     * 不含内链的内容保持原样一字不改。
     */
    private static function polishLinksOnly(\WP_Post $post, int $postId, int $userId): ToolResult
    {
        $content = $post->post_content;
        $plain = trim(strip_tags($content));
        $hasBlocks = (strpos($content, '<!-- wp:') !== false);

        if (empty($plain) && !$hasBlocks) {
            return ToolResult::error('文章内容为空');
        }

        // 保护 Gutenberg 块结构为占位符
        $blockMap = [];
        $content = preg_replace_callback(
            '/<!--\s*\/?wp:[^>]+-->/',
            function ($m) use (&$blockMap) {
                $key = '[[GB_' . count($blockMap) . ']]';
                $blockMap[$key] = $m[0];
                return $key;
            },
            $content
        );

        $system = "你是专业文章编辑。你的唯一任务是：让文章中的内链（<a class=\"xzd-tag-link\"）完美丝滑地融入文章。\n\n";
        $system .= "核心原则：\n";
        $system .= "1. 只修改包含 <a class=\"xzd-tag-link\" 的句子及其紧邻的1-2句上下文\n";
        $system .= "2. 其他不包含内链的内容必须保持原样，一字不改，一段不动\n";
        $system .= "3. 对于每个内链，判断它是否读起来自然：\n";
        $system .= "   - 如果已经自然融入段落中 → 不动\n";
        $system .= "   - 如果生硬或孤立（如底部「相关阅读」列表中的链接）→ 必须处理\n";
        $system .= "4. 处理底部链接列表的方法（重要）：\n";
        $system .= "   - 找到文中与该链接关键词相关的段落，将该链接自然融入该段落中\n";
        $system .= "   - 如果文中没有合适段落，在该链接前后各写1-2句，描述这个概念与文章主题的关系，使其成为一个独立的小段落\n";
        $system .= "   - 处理完后，删除原有的底部「相关阅读」链接列表标记（<!-- xzd-autolink-unmatched -->）\n";
        $system .= "5. 扩写规则：扩写内容必须与原文主题相关，承上启下，控制在20-40字，读起来像原文的自然延伸\n";
        $system .= "6. 输出完整文章HTML，保持所有格式、标签、图片、其他链接不变\n";
        $system .= "7. 保留所有 [[GB_N]] 格式的占位标记，这是文章结构标记，一个不能少、不能改";

        $user = "文章标题：{$post->post_title}\n\n文章内容：\n{$content}\n\n请优化每一个内链的融入度。把底部孤立的「相关阅读」链接分配到文章相关段落中，或为其扩写上下文。让文章读起来像这些链接本来就是文章的一部分。";

        try {
            $result = Client::instance()->chat(
                [Types::system($system), Types::user($user)],
                ['max_tokens' => 8000]
            );

            if (isset($result['error'])) return ToolResult::error($result['error']);

            $raw = $result['content'];
            $raw = preg_replace('/```[a-z]*\s*/i', '', $raw);
            $p1 = strpos($raw, '<');
            $p2 = strpos($raw, '[[');
            $pos = ($p1 !== false && $p2 !== false) ? min($p1, $p2) : ($p1 !== false ? $p1 : $p2);
            if ($pos === false) {
                return ToolResult::error('AI 返回内容格式异常，请重试');
            }
            $raw = substr($raw, $pos);
            $polished = trim($raw);

            if (empty($polished)) {
                return ToolResult::error('AI 返回内容为空');
            }

            // 还原 Gutenberg 块结构
            if (!empty($blockMap)) {
                $polished = str_replace(array_keys($blockMap), array_values($blockMap), $polished);
            }

            $originalLen = mb_strlen(strip_tags($post->post_content));
            $polishedLen = mb_strlen(strip_tags($polished));

            // 写回
            PreserveDates::updatePost([
                'ID'           => $postId,
                'post_content' => $polished,
            ]);

            update_post_meta($postId, '_xzd_ai_polished', $polished);

            return ToolResult::success([
                'original_len' => $originalLen,
                'polished_len' => $polishedLen,
            ], "内链融入优化完成（{$originalLen}→{$polishedLen}字）", [
                'tokens_used' => $result['input_tokens'] + $result['output_tokens'],
                'undo_data'   => ['post_id' => $postId],
            ]);
        } catch (\Exception $e) {
            return ToolResult::error('内链融入异常: ' . $e->getMessage());
        }
    }

    /**
     * 重建文本块 — 保留块类型和属性，只替换文本内容
     * 全部走适配器，不硬编码任何块名
     */
    private static function rebuildTextBlock(array $block, string $newText): ?array
    {
        $name = $block['blockName'] ?? '';
        $attrs = $block['attrs'] ?? '';

        // 块名 → 抽象类型 反向查找 (通过适配器的映射表)
        $mapping = GutenbergBlockBuilder::getBlockMapping();
        $reverseMap = array_flip($mapping);
        $abstractType = $reverseMap[$name] ?? '';

        // textBlocks列表也由适配器提供
        $textNames = GutenbergBlockBuilder::getTextBlockNames();
        if (!in_array($name, $textNames, true)) return null;

        switch ($abstractType) {
            case 'paragraph':
                return GutenbergBlockBuilder::paragraph($newText, is_array($attrs) ? $attrs : []);
            case 'heading':
                return GutenbergBlockBuilder::heading($newText, $attrs['level'] ?? 2, $attrs['anchor'] ?? null);
            case 'styled_heading':
                return GutenbergBlockBuilder::styledHeading($newText, $attrs['color'] ?? '#1fbc45', $attrs['tag_name'] ?? 'h2');
            case 'quote':
            case 'styled_quote':
                return GutenbergBlockBuilder::styledQuote($newText, $attrs['color'] ?? '');
            case 'alert':
                $style = 'info';
                if (is_array($attrs) && !empty($attrs['typeClass'])) {
                    $style = str_replace('alert alert-', '', $attrs['typeClass']);
                }
                return GutenbergBlockBuilder::alert($newText, $style, !empty($attrs['isChecked']));
            case 'pullquote':
                return GutenbergBlockBuilder::pullquote($newText);
            case 'preformatted':
                return GutenbergBlockBuilder::preformatted($newText);
            case 'list':
                $items = array_filter(array_map('trim', explode("\n", $newText)));
                $items = array_map(fn($s) => ['content' => ltrim($s, '-*• ') ?: $s], $items);
                return GutenbergBlockBuilder::listBlock($items, !empty($attrs['ordered']));
            default:
                // 通用降级：同名paragraph
                return GutenbergBlockBuilder::paragraph($newText);
        }
    }

    /**
     * 翻译内容 — 块感知，翻译结果存meta不影响原文章
     */
    public static function translateContent(array $params, int $userId): ToolResult
    {
        $postId = (int) ($params['post_id'] ?? 0);
        $targetLang = $params['target_lang'] ?? 'en';
        $procMode = $params['_processing_mode'] ?? 'fill_empty';
        $post = get_post($postId);
        if (!$post) return ToolResult::error("文章不存在: {$postId}");

        $langMap = ['en' => '英文', 'ja' => '日文', 'ko' => '韩文', 'zh' => '中文'];
        $langName = $langMap[$targetLang] ?? $targetLang;

        // 从块中提取纯文本
        $existingBlocks = parse_blocks($post->post_content);
        $textParts = [];
        foreach ($existingBlocks as $block) {
            $name = $block['blockName'] ?? '';
            $innerHTML = $block['innerHTML'] ?? '';
            if (in_array($name, ['core/paragraph', 'core/heading', 'core/list', 'core/quote',
                'zibllblock/biaoti', 'zibllblock/quote', 'zibllblock/alert'])) {
                $textParts[] = wp_strip_all_tags($innerHTML);
            }
        }

        $content = mb_substr(implode("\n\n", $textParts), 0, 3000);
        if (empty(trim($content))) {
            $content = mb_substr(wp_strip_all_tags(strip_shortcodes($post->post_content)), 0, 3000);
        }

        $result = Client::instance()->chat(
            [Types::user("将以下内容翻译为{$langName}，直接返回翻译结果：\n\n{$content}")],
            ['max_tokens' => 4000]
        );

        if (isset($result['error'])) return ToolResult::error($result['error']);

        $translated = trim($result['content']);
        $translated = preg_replace('/^```[a-z]*\s*/i', '', $translated);
        $translated = preg_replace('/\s*```$/', '', $translated);
        $key = '_xzd_ai_translated_' . $targetLang;

        if (!get_post_meta($postId, '_xzd_ai_original', true)) {
            update_post_meta($postId, '_xzd_ai_original', $post->post_content);
        }

        if ($procMode === 'append') {
            $existing = get_post_meta($postId, '_xzd_ai_translated_list', true) ?: [];
            $existing[] = ['lang' => $targetLang, 'lang_name' => $langName, 'content' => mb_substr($translated, 0, 200) . '...', 'full' => $translated, 'time' => current_time('mysql')];
            update_post_meta($postId, '_xzd_ai_translated_list', $existing);
            update_post_meta($postId, $key, $translated);
            $msg = "{$langName}翻译完成（追加为新版本，共 " . count($existing) . " 个翻译版本）";
        } else {
            update_post_meta($postId, $key, $translated);
            $msg = "{$langName}翻译完成（覆盖原有）";
        }

        // 翻译结果存meta，不修改原文章post_content
        return ToolResult::success(['translated' => mb_substr($translated, 0, 200) . '...', 'lang' => $targetLang, 'mode' => $procMode], $msg, [
            'tokens_used' => $result['input_tokens'] + $result['output_tokens'],
        ]);
    }

    /**
     * 改写内容 — 保留Gutenberg块结构
     */
    public static function rewriteContent(array $params, int $userId): ToolResult
    {
        $postId = (int) ($params['post_id'] ?? 0);
        $style = $params['style'] ?? 'professional';
        $procMode = $params['_processing_mode'] ?? 'fill_empty';
        $post = get_post($postId);
        if (!$post) return ToolResult::error("文章不存在: {$postId}");

        self::backupPostContent($postId);

        $styleMap = ['professional' => '专业化', 'casual' => '通俗化', 'creative' => '创意化'];
        $styleName = $styleMap[$style] ?? $style;

        // 解析块
        $existingBlocks = parse_blocks($post->post_content);
        $textBlockTypes = GutenbergBlockBuilder::getTextBlockNames();

        $textIndices = [];
        $textContents = [];
        $blockMaps = [];
        foreach ($existingBlocks as $i => $block) {
            $name = $block['blockName'] ?? '';
            if (in_array($name, $textBlockTypes) && !empty($block['innerHTML'])) {
                $textIndices[] = $i;
                $protected = self::protectBlocks($block['innerHTML']);
                $blockMaps[] = $protected['blocks'];
                $textContents[] = "[" . count($textIndices) . "] " . wp_strip_all_tags($protected['content']);
            }
        }

        if (empty($textContents)) {
            return ToolResult::error('文章没有可改写的内容');
        }

        $aiInput = "用{$styleName}风格改写以下文章。\n\n"
            . "⚠️ 保持每个段落的编号标记 [N]，每个段落独立改写。\n"
            . "{$styleName}风格要求重写表达方式和语气，但保持原意不变。\n\n"
            . implode("\n\n", $textContents);

        $result = Client::instance()->chat(
            [Types::user($aiInput)],
            ['max_tokens' => 4000]
        );

        if (isset($result['error'])) return ToolResult::error($result['error']);

        $rewrittenText = trim($result['content']);
        $rewrittenText = preg_replace('/^```[a-z]*\s*/i', '', $rewrittenText);
        $rewrittenText = preg_replace('/\s*```$/', '', $rewrittenText);

        $rewrittenParts = preg_split('/\n*(?=\[\d+\]\s)/', $rewrittenText);
        $rewrittenMap = [];
        foreach ($rewrittenParts as $part) {
            if (preg_match('/^\[(\d+)\]\s*(.+)$/s', trim($part), $m)) {
                $rewrittenMap[(int) $m[1]] = trim($m[2]);
            }
        }

        // 重建块
        $newBlocks = $existingBlocks;
        foreach ($textIndices as $seq => $origIdx) {
            $seqNum = $seq + 1;
            if (!isset($rewrittenMap[$seqNum])) continue;
            $newText = $rewrittenMap[$seqNum];
            // 还原被保护的代码块/图片/iframe
            if (isset($blockMaps[$seq]) && !empty($blockMaps[$seq])) {
                $newText = self::restoreBlocks($newText, $blockMaps[$seq]);
            }
            $newBlock = self::rebuildTextBlock($existingBlocks[$origIdx], $newText);
            if ($newBlock !== null) {
                $newBlocks[$origIdx] = $newBlock;
            }
        }

        if (!get_post_meta($postId, '_xzd_ai_original', true)) {
            update_post_meta($postId, '_xzd_ai_original', $post->post_content);
        }

        $newContent = serialize_blocks($newBlocks);
        $key = '_xzd_ai_rewritten_' . $style;

        if ($procMode === 'append') {
            $existing = get_post_meta($postId, '_xzd_ai_rewritten_list', true) ?: [];
            $existing[] = ['style' => $style, 'style_name' => $styleName, 'content' => mb_substr($newContent, 0, 200) . '...', 'full' => $newContent, 'time' => current_time('mysql')];
            update_post_meta($postId, '_xzd_ai_rewritten_list', $existing);
            $msg = "{$styleName}风格改写完成（追加为新版本，共 " . count($existing) . " 个改写版本）";
        } else {
            $msg = "{$styleName}风格改写完成（保留块结构）";
        }
        update_post_meta($postId, $key, $newContent);

        PreserveDates::updatePost([
            'ID'           => $postId,
            'post_content' => $newContent,
        ]);

        return ToolResult::success(['rewritten_blocks' => count($textIndices), 'style' => $style, 'mode' => $procMode], $msg, [
            'tokens_used' => $result['input_tokens'] + $result['output_tokens'],
        ]);
    }

    /**
     * 扩展内容 — 保留Gutenberg块结构
     */
    public static function expandContent(array $params, int $userId): ToolResult
    {
        $postId = (int) ($params['post_id'] ?? 0);
        $ratio = (float) ($params['ratio'] ?? 1.5);
        $procMode = $params['_processing_mode'] ?? 'fill_empty';
        $post = get_post($postId);
        if (!$post) return ToolResult::error("文章不存在: {$postId}");

        self::backupPostContent($postId);

        // 解析块
        $existingBlocks = parse_blocks($post->post_content);
        $textBlockTypes = GutenbergBlockBuilder::getTextBlockNames();

        $textIndices = [];
        $textContents = [];
        foreach ($existingBlocks as $i => $block) {
            $name = $block['blockName'] ?? '';
            if (in_array($name, $textBlockTypes) && !empty($block['innerHTML'])) {
                $textIndices[] = $i;
                $textContents[] = "[" . count($textIndices) . "] " . wp_strip_all_tags($block['innerHTML']);
            }
        }

        if (empty($textContents)) {
            return ToolResult::error('文章没有可扩展的内容');
        }

        $aiInput = "将以下文章内容扩展约{$ratio}倍，增加细节和示例，保持原意。\n\n"
            . "⚠️ 保持每个段落的编号标记 [N]，每个段落独立扩展。\n\n"
            . implode("\n\n", $textContents);

        $result = Client::instance()->chat(
            [Types::user($aiInput)],
            ['max_tokens' => 8000]
        );

        if (isset($result['error'])) return ToolResult::error($result['error']);

        $expandedText = trim($result['content']);
        $expandedText = preg_replace('/^```[a-z]*\s*/i', '', $expandedText);
        $expandedText = preg_replace('/\s*```$/', '', $expandedText);

        $expandedParts = preg_split('/\n*(?=\[\d+\]\s)/', $expandedText);
        $expandedMap = [];
        foreach ($expandedParts as $part) {
            if (preg_match('/^\[(\d+)\]\s*(.+)$/s', trim($part), $m)) {
                $expandedMap[(int) $m[1]] = trim($m[2]);
            }
        }

        // 重建块
        $newBlocks = $existingBlocks;
        foreach ($textIndices as $seq => $origIdx) {
            $seqNum = $seq + 1;
            if (!isset($expandedMap[$seqNum])) continue;
            $newBlock = self::rebuildTextBlock($existingBlocks[$origIdx], $expandedMap[$seqNum]);
            if ($newBlock !== null) {
                $newBlocks[$origIdx] = $newBlock;
            }
        }

        if (!get_post_meta($postId, '_xzd_ai_original', true)) {
            update_post_meta($postId, '_xzd_ai_original', $post->post_content);
        }

        $newContent = serialize_blocks($newBlocks);

        if ($procMode === 'append') {
            $existing = get_post_meta($postId, '_xzd_ai_expanded_list', true) ?: [];
            $existing[] = ['ratio' => $ratio, 'content' => mb_substr($newContent, 0, 200) . '...', 'full' => $newContent, 'time' => current_time('mysql')];
            update_post_meta($postId, '_xzd_ai_expanded_list', $existing);
            $msg = '扩展完成（追加为新版本，共 ' . count($existing) . ' 个扩展版本）';
        } else {
            $msg = '扩展完成（保留块结构）';
        }
        update_post_meta($postId, '_xzd_ai_expanded', $newContent);

        PreserveDates::updatePost([
            'ID'           => $postId,
            'post_content' => $newContent,
        ]);

        return ToolResult::success(['expanded_blocks' => count($textIndices), 'ratio' => $ratio, 'mode' => $procMode], $msg, [
            'tokens_used' => $result['input_tokens'] + $result['output_tokens'],
        ]);
    }

    /**
     * 生成封面图
     *
     * mode=prompt (默认，用于流水线): 仅生成描述词，存入 _xzd_ai_cover_prompt
     * mode=full  (手动，用于编辑器): 完整管线 → 生成描述词 → 获取图片 → 下载 → 设为特色图
     */
    public static function generateCover(array $params, int $userId): ToolResult
    {
        $postId = (int) ($params['post_id'] ?? 0);
        $style  = $params['style'] ?? '简约';
        $mode   = $params['mode'] ?? 'prompt';
        $source = $params['source'] ?? null;  // ai|stock|public
        $post   = get_post($postId);
        if (!$post) return ToolResult::error("文章不存在: {$postId}");

        $imgClient = \XZD\AI\V5\AI\ImageClient::instance();

        // 完整模式：先检查来源（无需先消耗 API 生成 prompt）
        if ($mode === 'full') {
            $hasAI = $imgClient->hasImageProvider();

            // 未指定来源 且 AI 可用 → 自动化上下文直接使用 AI，对话交互中弹出选择卡片
            if (!$source && $hasAI) {
                $isInteractive = !empty($params['_interactive']);
                if ($isInteractive) {
                    $sources = $imgClient->getAvailableSources();
                    return ToolResult::success([
                        'post_id'          => $postId,
                        'source_selection' => true,
                        'choices'          => $sources,
                    ], '请选择封面图来源 — 检测到已配置 AI 生图模型');
                }
                // 自动化流水线：直接走 AI 生图 → 自动降级链（stock → public → placeholder）
                $source = 'ai';
            }

            // 未指定来源 且 无 AI → 自动降级，告知用户
            if (!$source && !$hasAI) {
                $source = 'stock';
            }
        }

        // 生成封面描述 prompt — V3.1.7: 优先使用摘要+标题确保相关性
        $content = mb_substr(wp_strip_all_tags(strip_shortcodes($post->post_content)), 0, 2000);
        $summary = get_post_meta($postId, '_xzd_ai_summary', true) ?: '';
        $summaryHint = $summary ? "\n文章摘要：{$summary}" : '';
        $result = Client::instance()->chat(
            [Types::user(
                "你是一个封面图设计师。根据以下文章信息生成1个AI生图英文描述词(prompt)。"
                . "要求：图片内容必须直接体现文章主题和核心内容，不能只是抽象背景。100字以内英文，只返回prompt："
                . "\n\n标题：{$post->post_title}{$summaryHint}"
                . "\n内容：{$content}"
            )],
            ['max_tokens' => 200]
        );

        if (isset($result['error'])) return ToolResult::error($result['error']);

        $prompt = trim($result['content']);
        $procMode = $params['_processing_mode'] ?? 'fill_empty';

        if ($procMode === 'append') {
            $existingPrompts = get_post_meta($postId, '_xzd_ai_cover_prompts', true) ?: [];
            $existingPrompts[] = ['prompt' => $prompt, 'style' => $style, 'time' => current_time('mysql')];
            update_post_meta($postId, '_xzd_ai_cover_prompts', $existingPrompts);
            update_post_meta($postId, '_xzd_ai_cover_prompt', $prompt);
        } else {
            update_post_meta($postId, '_xzd_ai_cover_prompt', $prompt);
        }

        // 快捷模式：仅prompt
        if ($mode === 'prompt') {
            return ToolResult::success(
                ['cover_prompt' => $prompt, 'mode' => 'prompt'],
                '封面描述词生成成功' . ($procMode === 'append' ? '（追加为新版本）' : '（覆盖原有）'),
                ['tokens_used' => $result['input_tokens'] + $result['output_tokens']]
            );
        }

        // 完整模式：prompt → 图片 → 下载 → 特色图
        $hasAI = $imgClient->hasImageProvider();
        $sourceLabel = ['ai' => 'AI 生图', 'stock' => '免费图库', 'public' => '公共域图库'][$source] ?? $source;
        $imageResult = $imgClient->generateCoverImage($postId, [
            'style'            => $style,
            'preferred_source' => $source,
            'prompt'           => $prompt,  // V3.1.7修复：复用已生成的prompt，避免ImageClient重复AI调用
        ]);

        if (!$imageResult['success']) {
            return ToolResult::error($imageResult['message'] ?? '封面图生成失败');
        }

        $sourceNote = $hasAI ? '' : '（未配置 AI 生图模型，已自动使用' . $sourceLabel . '）';

        return ToolResult::success([
            'cover_prompt'   => $prompt,
            'attachment_id'  => $imageResult['attachment_id'],
            'image_url'      => $imageResult['url'],
            'source'         => $imageResult['source'],
            'mode'           => 'full',
        ], "封面图已生成并设置 (来源: {$sourceLabel}){$sourceNote}", [
            'tokens_used'   => $result['input_tokens'] + $result['output_tokens'],
            'undo_data'     => ['reverse_tool' => 'wp_delete_post', 'reverse_params' => ['post_id' => $imageResult['attachment_id']]],
        ]);
    }

    /**
     * 生成 ALT 文字 — Vision AI 视觉分析 + 回写到文章 HTML
     */
    public static function generateALT(array $params, int $userId): ToolResult
    {
        $postId = (int) ($params['post_id'] ?? 0);
        $post = get_post($postId);
        if (!$post) return ToolResult::error("文章不存在: {$postId}");

        preg_match_all('/<img[^>]+src=["\']([^"\']+)["\'][^>]*>/i', $post->post_content, $matches);
        if (empty($matches[1])) {
            return ToolResult::success(['images' => 0], '文章中没有图片');
        }

        $images = array_slice($matches[1], 0, 10);
        $altTexts = [];
        $content = $post->post_content;
        $failed = 0;

        foreach ($images as $imgUrl) {
            $visionResult = \XZD\AI\V5\AI\ImageClient::instance()->analyze(
                $imgUrl,
                "请用中文描述这张图片，适合用作 alt 属性。简洁，30字以内。"
            );

            $altText = '';
            if ($visionResult['success'] && !empty($visionResult['alt_text'])) {
                $altText = $visionResult['alt_text'];
            } else {
                $result = Client::instance()->chat(
                    [Types::user("为图片生成中文alt文字，30字以内：\n图片URL: {$imgUrl}\n文章标题: {$post->post_title}")],
                    ['max_tokens' => 50]
                );
                if (!isset($result['error'])) {
                    $altText = trim($result['content']);
                }
            }

            if (!empty($altText)) {
                $altTexts[$imgUrl] = $altText;

                $escapedAlt = esc_attr($altText);
                // 在内容中找到该图片标签
                $imgPattern = '/(<img[^>]*src=["\']' . preg_quote($imgUrl, '/') . '["\'][^>]*)(\/?)>/i';
                if (preg_match($imgPattern, $content, $imgMatch)) {
                    $fullTag = $imgMatch[0];
                    if (preg_match('/\salt=["\'][^"\']*["\']/i', $fullTag)) {
                        // 该图片已有 alt → 替换值
                        $newTag = preg_replace(
                            '/\salt=["\'][^"\']*["\']/i',
                            ' alt="' . $escapedAlt . '"',
                            $fullTag
                        );
                    } else {
                        // 该图片无 alt → 添加
                        $newTag = preg_replace(
                            '/\s*\/?>$/i',
                            ' alt="' . $escapedAlt . '"$0',
                            $fullTag
                        );
                    }
                    $content = str_replace($fullTag, $newTag, $content);
                }
            } else {
                $failed++;
            }
        }

        update_post_meta($postId, '_xzd_ai_alt_texts', $altTexts);

        if (!empty($altTexts)) {
            PreserveDates::updatePost([
                'ID'           => $postId,
                'post_content' => $content,
            ]);
        }

        $successCount = count($altTexts);
        $totalCount = count($images);
        if ($successCount === 0 && $totalCount > 0) {
            return ToolResult::success(
                ['images' => 0, 'total' => $totalCount, 'failed' => $failed],
                "所有 {$totalCount} 张图片的 ALT 生成均失败（AI视觉分析不可用），图片URL已记录至 post_meta _xzd_ai_alt_texts"
            );
        }

        $msg = "为 {$successCount} 张图片生成了 ALT 文字并写入文章HTML";
        if ($failed > 0) {
            $msg .= "（{$failed} 张失败）";
        }
        return ToolResult::success(
            ['images' => $successCount, 'total' => $totalCount, 'failed' => $failed, 'alt_texts' => $altTexts],
            $msg
        );
    }

    /**
     * 构建内链
     */
    /**
     * 构建内链 — 关键词匹配模式
     *
     * 双来源（SEO关键词 + 文章标签）→ 去重合并 → 长词优先 →
     * 正则匹配在正文中插入链接 → 未匹配关键词附文末
     *
     * 参数:
     *   post_id, source (tags|keywords|mixed), tag_count, seo_count,
     *   max_total, max_per_word, link_color, link_weight, _processing_mode
     */
    public static function buildAutoLink(array $params, int $userId): ToolResult
    {
        $postId = (int) ($params['post_id'] ?? 0);
        $post = get_post($postId);
        if (!$post) return ToolResult::error("文章不存在: {$postId}");

        if (empty(trim(strip_tags($post->post_content)))) {
            return ToolResult::error('文章内容为空');
        }

        self::backupPostContent($postId);

        // ═══ 配置读取（优先用传入参数，其次用全局配置） ═══
        $config = \XZD\AI\V5\Core\Config::instance();
        $source     = $params['source'] ?? $config->get('autolink_source', 'mixed');
        $tagCount   = (int) ($params['tag_count'] ?? $config->get('autolink_tag_count', 3));
        $seoCount   = (int) ($params['seo_count'] ?? $config->get('autolink_seo_count', 3));
        $maxTotal   = (int) ($params['max_total'] ?? $config->get('autolink_max_total', 8));
        $maxPerWord = (int) ($params['max_per_word'] ?? $config->get('autolink_max_per_word', 1));
        $linkColor  = $params['link_color'] ?? $config->get('autolink_link_color', '#2F6BFF');
        $linkWeight = $params['link_weight'] ?? $config->get('autolink_link_weight', '500');
        $textDecoration = $params['text_decoration'] ?? $config->get('autolink_text_decoration', 'underline');
        $libThreshold = (int) ($config->get('autolink_lib_threshold', 60));

        // ═══ Step 1: 关键词收集 ═══
        $tagKeywords = [];
        $seoKeywords = [];

        // 来源：标签
        if (in_array($source, ['tags', 'mixed'])) {
            $tagObjs = wp_get_post_tags($postId, ['fields' => 'names']);
            if (!is_wp_error($tagObjs) && !empty($tagObjs)) {
                $tagKeywords = array_map('trim', $tagObjs);
                if ($tagCount > 0) {
                    $tagKeywords = array_slice($tagKeywords, 0, $tagCount);
                }
            }
        }

        // 来源：SEO关键词
        if (in_array($source, ['keywords', 'mixed'])) {
            $kwStr = get_post_meta($postId, '_xzd_ai_seo_keywords', true);
            if (!empty($kwStr)) {
                $seoKeywords = array_map('trim', explode(',', str_replace('，', ',', $kwStr)));
                $seoKeywords = array_filter($seoKeywords);
                if ($seoCount > 0) {
                    $seoKeywords = array_slice($seoKeywords, 0, $seoCount);
                }
            }
        }

        // 合并去重（SEO关键词优先，标签补充）
        $seen = [];
        $unique = [];
        $seoAdded = 0;
        foreach ($seoKeywords as $k) {
            $lower = mb_strtolower($k);
            if (!isset($seen[$lower])) { $seen[$lower] = true; $unique[] = $k; $seoAdded++; }
        }
        $tagAdded = 0;
        foreach ($tagKeywords as $k) {
            $lower = mb_strtolower($k);
            if (!isset($seen[$lower])) { $seen[$lower] = true; $unique[] = $k; $tagAdded++; }
        }

        if (empty($unique)) {
            return ToolResult::error('没有可用的关键词。请先生成标签或SEO关键词。');
        }

        // 长词优先
        usort($unique, function($a, $b) { return mb_strlen($b) - mb_strlen($a); });

        // 总数限制
        if ($maxTotal > 0 && count($unique) > $maxTotal) {
            $unique = array_slice($unique, 0, $maxTotal);
        }

        // ═══ Step 2: 正则匹配在正文中插入内链 ═══
        $content = $post->post_content;
        $totalInserted = 0;
        $unmatched = [];

        foreach ($unique as $keyword) {
            $inserted = 0;
            $term = get_term_by('name', $keyword, 'post_tag');
            $url = ($term && !is_wp_error($term)) ? get_tag_link($term) : home_url('/?s=' . urlencode($keyword));
            $linkHtml = sprintf(
                '<a href="%s" class="xzd-tag-link">%s</a>',
                esc_url($url), esc_attr($linkColor), esc_attr($linkWeight), esc_attr($textDecoration), esc_html($keyword)
            );

            $content = self::replaceKeywordInHTML($content, $keyword, $linkHtml, $maxPerWord, $inserted);
            $totalInserted += $inserted;

            if ($inserted === 0) {
                $unmatched[] = $keyword;
            }
        }

        // ═══ Step 3: 未匹配关键词作为段落附文末 ═══
        $appended = 0;
        if (!empty($unmatched)) {
            usort($unmatched, function($a, $b) { return mb_strlen($b) - mb_strlen($a); });

            $linkItems = [];
            foreach ($unmatched as $kw) {
                $term = get_term_by('name', $kw, 'post_tag');
                $url = ($term && !is_wp_error($term)) ? get_tag_link($term) : home_url('/?s=' . urlencode($kw));
                $linkItems[] = sprintf(
                    '<a href="%s" class="xzd-tag-link">%s</a>',
                    esc_url($url), esc_attr($linkColor), esc_attr($linkWeight), esc_attr($textDecoration), esc_html($kw)
                );
            }

            $linksHtml = "\n\n<!-- xzd-autolink-unmatched -->\n";
            $linksHtml .= "<!-- wp:paragraph -->\n";
            $linksHtml .= "<p>相关阅读：" . implode('、', $linkItems) . "</p>\n";
            $linksHtml .= "<!-- /wp:paragraph -->\n";
            $linksHtml .= "<!-- /xzd-autolink-unmatched -->";

            // 替换或追加 unmatched 区块
            if (strpos($content, '<!-- xzd-autolink-unmatched -->') !== false) {
                $content = preg_replace(
                    '/<!-- xzd-autolink-unmatched -->.*?<!-- \/xzd-autolink-unmatched -->/s',
                    $linksHtml, $content
                );
            } else {
                $content .= $linksHtml;
            }
            $appended = count($unmatched);
        }

        // ═══ Step 4: 写回文章 ═══
        if ($content !== $post->post_content) {
            PreserveDates::updatePost([
                'ID'           => $postId,
                'post_content' => $content,
            ]);
            clean_post_cache($postId);
        }

        // 标记
        update_post_meta($postId, '_xzd_ai_autolink_done', '1');
        update_post_meta($postId, '_xzd_ai_autolinks', [
            'keywords' => $unique,
            'inserted' => $totalInserted,
            'appended' => $appended,
            'source'   => $source,
            'time'     => current_time('mysql'),
        ]);

        $msg = sprintf(
            '关键词内链完成：SEO%d个+标签%d个→去重%d个→文中插入%d处%s',
            $seoAdded, $tagAdded, count($unique), $totalInserted,
            $appended > 0 ? "，底部附加{$appended}个（可用「仅优化内链」润色融入）" : ''
        );

        return ToolResult::success([
            'keywords'  => $unique,
            'inserted'  => $totalInserted,
            'appended'  => $appended,
            'unmatched' => $unmatched,
            'source'    => $source,
        ], $msg);
    }

    /**
     * 在HTML文本区域中替换关键词为链接标签
     * 跳过 script/style/code/pre/textarea 等标签内部
     * 边界检查防止子串误匹配
     */
    private static function replaceKeywordInHTML(string $html, string $keyword, string $replacement, int $max, int &$count): string
    {
        $count = 0;
        if ($max <= 0 || mb_strlen($keyword) < 1) return $html;

        $skipTags = ['script', 'style', 'code', 'pre', 'textarea', 'noscript'];
        $parts = preg_split('/(<\/?[^>]+>)/', $html, -1, PREG_SPLIT_DELIM_CAPTURE);
        $result = '';
        $skipDepth = 0;

        foreach ($parts as $part) {
            if ($part === '' || $part === null) continue;

            if ($part[0] === '<') {
                $tagName = strtolower(preg_replace('/^<\/?(\w+).*$/s', '$1', $part));
                if (in_array($tagName, $skipTags, true)) {
                    if (substr($part, 0, 2) === '</') {
                        $skipDepth = max(0, $skipDepth - 1);
                    } else {
                        $skipDepth++;
                    }
                }
                $result .= $part;
                continue;
            }

            if ($skipDepth > 0) {
                $result .= $part;
                continue;
            }

            if ($count < $max) {
                $text = $part;
                $replacedText = '';
                $pos = 0;
                $kwLen = mb_strlen($keyword);

                while ($pos < mb_strlen($text) && $count < $max) {
                    $found = mb_stripos($text, $keyword, $pos);
                    if ($found === false) {
                        $replacedText .= mb_substr($text, $pos);
                        break;
                    }

                    $before = $found > 0 ? mb_substr($text, $found - 1, 1) : '';
                    $after = ($found + $kwLen) < mb_strlen($text) ? mb_substr($text, $found + $kwLen, 1) : '';

                    if (self::isWordBoundaryChar($before) || self::isWordBoundaryChar($after)) {
                        $replacedText .= mb_substr($text, $pos, $found - $pos + $kwLen);
                        $pos = $found + $kwLen;
                    } else {
                        $replacedText .= mb_substr($text, $pos, $found - $pos);
                        $replacedText .= $replacement;
                        $pos = $found + $kwLen;
                        $count++;
                    }
                }

                $result .= $replacedText;
            } else {
                $result .= $part;
            }
        }

        return $result;
    }

    /**
     * 判断字符是否为词语边界字符（中文汉字/字母/数字 → 不能作为关键词边界）
     */
    private static function isWordBoundaryChar(string $ch): bool
    {
        if ($ch === '') return false;
        $code = mb_ord($ch);
        if ($code >= 0x4E00 && $code <= 0x9FFF) return true;
        if ($code >= 0x3400 && $code <= 0x4DBF) return true;
        if (preg_match('/[a-zA-Z0-9]/', $ch)) return true;
        return false;
    }

    /**
     * 质量评分
     */
    public static function scoreQuality(array $params, int $userId): ToolResult
    {
        $postId = (int) ($params['post_id'] ?? 0);
        $post = get_post($postId);
        if (!$post) return ToolResult::error("文章不存在: {$postId}");

        $content = mb_substr(wp_strip_all_tags(strip_shortcodes($post->post_content)), 0, 5000);

        // V16：防御 AI 客户端内部异常，失败时降级到普通 chat
        $result = null;
        try {
            $result = Client::instance()->chatStructured(
                [Types::user("对以下文章内容进行质量评分（0-100），返回 JSON：\n\n{$content}")],
                [
                    'type' => 'object',
                    'properties' => [
                        'overall' => ['type' => 'integer'],
                        'readability' => ['type' => 'integer'],
                        'originality' => ['type' => 'integer'],
                        'structure' => ['type' => 'integer'],
                        'suggestions' => ['type' => 'array', 'items' => ['type' => 'string']],
                    ],
                    'required' => ['overall', 'readability', 'originality', 'structure'],
                ],
                ['max_tokens' => 500]
            );
        } catch (\Throwable $e) {
            Logger::warning('score_quality', 'AI结构化调用异常，降级到普通chat: ' . $e->getMessage(), ['post_id' => $postId]);
            try {
                $fallbackPrompt = "对以下文章内容进行质量评分（0-100），返回 JSON：{\"overall\":80,\"readability\":75,\"originality\":70,\"structure\":85,\"suggestions\":[\"建议1\",\"建议2\"]}\n\n{$content}";
                $rawResult = Client::instance()->chat(
                    [Types::user($fallbackPrompt)],
                    ['max_tokens' => 500]
                );
                if (!isset($rawResult['error'])) {
                    $text = $rawResult['content'] ?? '';
                    $parsed = null;
                    if (preg_match('/```(?:json)?\s*(\{[\s\S]*?\})\s*```/', $text, $m)) {
                        $parsed = json_decode($m[1], true);
                    }
                    if (!$parsed) {
                        $parsed = json_decode($text, true);
                    }
                    if ($parsed && isset($parsed['overall'])) {
                        $result = array_merge($rawResult, ['content' => $parsed]);
                    }
                }
            } catch (\Throwable $e2) {
                Logger::error('score_quality', '降级chat也失败: ' . $e2->getMessage());
            }
            if (!$result) {
                return ToolResult::error('AI 调用异常：' . $e->getMessage());
            }
        }

        if (isset($result['error'])) return ToolResult::error($result['error']);

        // V16：防御 AI 返回非数组格式
        $scores = $result['content'];
        if (!is_array($scores)) {
            return ToolResult::error('AI 返回格式异常，无法解析评分数据');
        }

        update_post_meta($postId, '_xzd_ai_quality_score', $scores);

        return ToolResult::success($scores, "质量评分: " . ($scores['overall'] ?? 'N/A') . "分");
    }

    /**
     * 去重检测
     */
    public static function checkDuplicate(array $params, int $userId): ToolResult
    {
        $postId = (int) ($params['post_id'] ?? 0);
        $post = get_post($postId);
        if (!$post) return ToolResult::error("文章不存在: {$postId}");

        $content = mb_substr(wp_strip_all_tags(strip_shortcodes($post->post_content)), 0, 2000);

        // 搜索相似文章
        $searchQuery = new \WP_Query([
            's'              => mb_substr(wp_strip_all_tags($post->post_title), 0, 50),
            'post_type'      => 'post',
            'post_status'    => 'publish',
            'posts_per_page' => 10,
            'post__not_in'   => [$postId],
        ]);

        $similars = [];
        while ($searchQuery->have_posts()) {
            $searchQuery->the_post();
            $similarContent = wp_strip_all_tags(strip_shortcodes(get_the_content()));
            similar_text($content, mb_substr($similarContent, 0, 2000), $percent);
            if ($percent > 30) {
                $similars[] = ['id' => get_the_ID(), 'title' => get_the_title(), 'similarity' => round($percent, 1)];
            }
        }
        wp_reset_postdata();

        return ToolResult::success(['duplicates' => $similars, 'count' => count($similars)], count($similars) > 0 ? "发现 " . count($similars) . " 篇相似文章" : '未发现重复内容');
    }

    /**
     * AI 写文章 — 输出Gutenberg块格式
     */
    /**
     * 标题扩写 — 根据文章标题和内容生成备选标题
     */
    public static function expandTitle(array $params, int $userId): ToolResult
    {
        $postId = (int) ($params['post_id'] ?? 0);
        $title = $params['title'] ?? '';
        $content = $params['content'] ?? '';
        $count = (int) ($params['count'] ?? 3);

        $post = get_post($postId);
        if ($post && empty($content)) {
            $content = mb_substr(wp_strip_all_tags(strip_shortcodes($post->post_content)), 0, 3000);
        }
        if ($post && empty($title)) {
            $title = $post->post_title;
        }

        if (empty($title) && empty($content)) {
            return ToolResult::error('文章标题和内容均为空，无法生成标题');
        }

        $suggestions = [];

        try {
            if (empty($content) && !empty($title)) {
                $system = "你是专业文章标题优化师。根据原标题生成{$count}个更吸引眼球的标题变体。要求：每个标题15-25个汉字；纯文本输出每行一个；不要带编号；与原标题语义相关但表达不同。";
                $user = "原标题：「{$title}」\n请生成{$count}个变体标题。";
            } else {
                $system = "你是专业文章标题优化师。根据文章内容生成{$count}个吸引眼球的标题建议。要求：每个标题15-25个汉字；纯文本输出每行一个；不要带编号。";
                if (!empty($title)) {
                    $system .= "原标题是「{$title}」，请生成与原标题不同但更优的标题。";
                }
                $contentSnippet = mb_substr($content, 0, 3000);
                $user = "文章内容：\n{$contentSnippet}";
            }

            $messages = [
                Types::system($system),
                Types::user($user),
            ];

            $resp = Client::instance()->chat($messages, ['max_tokens' => 500]);

            if (isset($resp['content']) && !empty($resp['content'])) {
                $lines = explode("\n", trim($resp['content']));
                foreach ($lines as $line) {
                    $line = trim($line);
                    $line = preg_replace('/^[\d]+[\.\、\)\s\-]+/u', '', $line);
                    $line = preg_replace('/^[\-\*\•]\s*/u', '', $line);
                    $line = trim($line);
                    if (!empty($line) && mb_strlen($line) >= 5) {
                        $suggestions[] = $line;
                    }
                }
            }
        } catch (\Exception $e) {
            // V3.1.7: 记录错误日志，便于排查
            Logger::warning('content_manager', 'expandTitle AI调用失败，降级使用默认标题', [
                'error' => $e->getMessage(),
            ]);
        }

        if (empty($suggestions)) {
            if (!empty($title)) {
                $suggestions = [$title, $title . ' | 全面解读', '深入理解' . $title];
            } else {
                return ToolResult::error('标题生成失败，请手动输入');
            }
        }

        $suggestions = array_slice(array_unique($suggestions), 0, $count);

        return ToolResult::success([
            'suggestions' => $suggestions,
            'original'    => $title,
        ], '生成了 ' . count($suggestions) . ' 个备选标题');
    }

    /**
     * 编辑建议 — 逐段分析文章的可读性/SEO/语法问题
     */
    public static function editorialNotes(array $params, int $userId): ToolResult
    {
        $postId = (int) ($params['post_id'] ?? 0);
        $post = get_post($postId);
        if (!$post) return ToolResult::error("文章不存在: {$postId}");

        $plain = wp_strip_all_tags(strip_shortcodes($post->post_content));
        if (empty(trim($plain))) {
            return ToolResult::error('文章内容为空');
        }

        // 拆为段落块（每200字一块，最多10块）
        $chunks = [];
        $len = mb_strlen($plain);
        for ($i = 0; $i < $len; $i += 200) {
            $chunk = trim(mb_substr($plain, $i, 200));
            if (!empty($chunk)) {
                $chunks[] = $chunk;
            }
        }
        if (count($chunks) > 10) {
            $chunks = array_slice($chunks, 0, 10);
        }

        $system = "你是资深内容编辑。对文章逐段分析，从三个维度给出修改建议：\n";
        $system .= "- 可读性(readability)：句子长度、表达清晰度、冗长难懂表述\n";
        $system .= "- SEO(seo)：关键词密度、标题结构、内容覆盖度\n";
        $system .= "- 语法(grammar)：错别字、语病、标点错误\n\n";
        $system .= "返回JSON（不要markdown）：\n";
        $system .= "{\"suggestions\":[{\"text\":\"原文片段\",\"type\":\"readability|seo|grammar\",\"note\":\"具体修改建议\"}]}\n";
        $system .= "每条建议引用原文具体片段，最多返回8条高优先级建议。";

        $parasText = '';
        foreach ($chunks as $idx => $p) {
            $parasText .= "[段落{$idx}] {$p}\n\n";
        }

        try {
            $messages = [Types::system($system), Types::user("标题：{$post->post_title}\n内容（共" . count($chunks) . "段）：\n{$parasText}\n请给出编辑建议。")];
            $resp = Client::instance()->chat($messages, ['max_tokens' => 2000]);

            if (!isset($resp['content']) || empty($resp['content'])) {
                return ToolResult::error('AI 未返回内容');
            }

            $raw = $resp['content'];
            $raw = preg_replace('/```(?:json)?\s*/i', '', $raw);
            $jsonStart = strpos($raw, '{');
            if ($jsonStart === false) {
                return ToolResult::error('AI 返回格式异常，请重试');
            }
            $raw = substr($raw, $jsonStart);
            $data = json_decode($raw, true);

            if (!$data || empty($data['suggestions'])) {
                return ToolResult::error('未生成有效建议');
            }

            $suggestions = array_slice($data['suggestions'], 0, 8);
            $counts = ['readability' => 0, 'seo' => 0, 'grammar' => 0];
            foreach ($suggestions as $s) {
                $type = $s['type'] ?? 'readability';
                $counts[$type] = ($counts[$type] ?? 0) + 1;
            }

            return ToolResult::success([
                'suggestions' => $suggestions,
                'count'       => count($suggestions),
                'summary'     => $counts,
            ], '分析完成，共 ' . count($suggestions) . ' 条建议');
        } catch (\Exception $e) {
            return ToolResult::error('分析异常: ' . $e->getMessage());
        }
    }

    public static function writeArticle(array $params, int $userId): ToolResult
    {
        $topic = $params['topic'] ?? '';
        $keywords = $params['keywords'] ?? '';
        $categoryId = (int) ($params['category_id'] ?? 0);
        $status = $params['post_status'] ?? 'publish';
        $tags = $params['tags'] ?? '';
        $illustrationCount = (int) ($params['illustration_count'] ?? 0);
        $imageSource = $params['image_source'] ?? 'stock';
        $generateCover = (bool) ($params['generate_cover'] ?? true);
        $generateSeo = (bool) ($params['generate_seo'] ?? true);
        $dailyMax = (int) ($params['daily_max'] ?? 3);

        if (empty($topic)) {
            return ToolResult::error('请提供文章主题');
        }

        // 安全机制：日限额检查
        if ($dailyMax > 0) {
            $todayCount = ScheduleWriteService::getTodayWrittenCount();
            if ($todayCount >= $dailyMax) {
                return ToolResult::error(
                    "今日已生成 {$todayCount}/{$dailyMax} 篇文章，已达上限",
                    'daily_max_reached',
                    ['skipped' => true, 'today_count' => $todayCount, 'daily_max' => $dailyMax]
                );
            }
        }

        // AI提示词由适配器提供（Zibll有Zibll的块，通用主题有通用块）
        $guidelines = GutenbergBlockBuilder::getAIBlockGuidelines();
        $systemPrompt = "你是专业的技术内容创作者。你需要为文章生成Gutenberg块内容，返回JSON数组。\n\n"
            . $guidelines
            . "\n\n## 块选用原则（必须遵守！）\n"
            . "- 文章10-25个块，内容充实。paragraph占50%以下，其余用丰富的块类型\n"
            . "- 段落100-300字，信息密度高，避免啰嗦\n"
            . "- content字段使用纯HTML格式(<p>/<h2>/<h3>/<ul>/<ol>/<li>/<strong>/<em>/<a>/<blockquote>/<pre><code>)，绝对禁止使用Markdown格式(#/**/-/``)";

        $userPrompt = "写一篇关于\"{$topic}\"的文章";
        if ($keywords) {
            $userPrompt .= "，包含以下关键词：{$keywords}";
        }
        $userPrompt .= "。800-2000字，按结构化JSON输出。";

        // AI生成 + 重试逻辑
        $maxRetries = 1;
        $attempt = 0;
        $result = null;

        while ($attempt <= $maxRetries) {
            $result = Client::instance()->chatStructured(
                [
                    Types::system($systemPrompt),
                    Types::user($userPrompt),
                ],
                [
                    'type' => 'array',
                    'items' => [
                        'type' => 'object',
                        'properties' => [
                            'type' => ['type' => 'string'],
                            'content' => ['type' => 'string'],
                            'level' => ['type' => 'integer'],
                            'color' => ['type' => 'string'],
                            'tag' => ['type' => 'string'],
                            'language' => ['type' => 'string'],
                            'code' => ['type' => 'string'],
                            'title' => ['type' => 'string'],
                            'note' => ['type' => 'string'],
                            'icon' => ['type' => 'string'],
                            'style' => ['type' => 'string'],
                            'ordered' => ['type' => 'boolean'],
                            'items' => ['type' => 'array'],
                            'headers' => ['type' => 'array'],
                            'rows' => ['type' => 'array'],
                            'citation' => ['type' => 'string'],
                            'linenumbers' => ['type' => 'boolean'],
                            'closeable' => ['type' => 'boolean'],
                            'open' => ['type' => 'boolean'],
                        ],
                    ],
                ],
                ['max_tokens' => 8000]
            );

            if (!isset($result['error']) && !empty($result['content'])) {
                break;
            }

            $attempt++;
            if ($attempt <= $maxRetries) {
                Logger::warning('write_article', "AI生成失败（第{$attempt}次重试）: " . ($result['error'] ?? 'empty_content'));
                sleep(3);
            }
        }

        // 重试耗尽仍失败 → 降级
        if ($attempt > $maxRetries || (isset($result['error']) && empty($result['content']))) {
            Logger::error('write_article', "AI生成失败，已重试{$maxRetries}次，降级到旧格式");
            \XZD\AI\V5\Core\NotificationThrottle::send(
                'write_article_failed',
                '定时写文章AI调用失败',
                "主题: {$topic}\n错误: " . ($result['error'] ?? 'AI未返回内容') . "\n已自动降级处理"
            );
            return self::writeArticleLegacy($params, $userId);
        }

        $aiBlocks = $result['content'] ?? [];
        if (empty($aiBlocks)) {
            return self::writeArticleLegacy($params, $userId);
        }

        // 提取标题（从第一个标题块，适配所有主题）
        $title = $topic;
        $adapter = \XZD\AI\V5\Theme\AdapterRegistry::getCurrent();
        $headingTypes = $adapter->getTextBlockNames();
        foreach ($aiBlocks as $block) {
            // V3.1.7 防御：处理非数组块（字符串→检查是否可作为标题）
            if (!is_array($block)) {
                if (is_string($block) && preg_match('/^[#]{1,3}\s+(.+)/u', trim($block), $m)) {
                    $title = trim($m[1]) ?: $topic;
                }
                continue;
            }
            $blockType = $block['type'] ?? '';
            // 匹配任意标题块（core/heading, zibllblock/biaoti, 或抽象类型 heading/zibll_biaoti）
            $isHeading = in_array($blockType, $headingTypes)
                || stripos($blockType, 'heading') !== false
                || stripos($blockType, 'biaoti') !== false;
            if ($isHeading) {
                $title = $block['content'] ?? $topic;
                break;
            }
        }

        // AI JSON → Gutenberg块
        $blocks = GutenbergBlockBuilder::fromStructuredJSON($aiBlocks);

        // 安全网: 如果只产生1个块且内容是Markdown，重新解析
        if (count($blocks) === 1 && isset($blocks[0]['innerHTML'])) {
            $innerHTML = $blocks[0]['innerHTML'];
            $hasMd = preg_match('/^#{1,6}\s|^\*\*|^-\s|^```/m', strip_tags($innerHTML));
            if ($hasMd) {
                $plainText = strip_tags($innerHTML);
                $blocks = self::parseMarkdownToBlocks($plainText, $title);
            }
        }

        // 内容质量验证
        $plainContent = wp_strip_all_tags(GutenbergBlockBuilder::serialize($blocks));
        if (mb_strlen($plainContent) < 100) {
            Logger::warning('write_article', "AI生成内容过短（" . mb_strlen($plainContent) . "字符），视为失败");
            return ToolResult::error('AI生成内容过短（' . mb_strlen($plainContent) . '字符），请优化主题描述后重试');
        }

        // 先创建草稿获得 post_id（媒体库依赖）
        $postData = [
            'post_title'   => $title,
            'post_content' => '', // 占位，稍后更新
            'post_status'  => 'draft',
            'post_type'    => 'post',
            'post_author'  => $userId,
        ];
        if ($categoryId) {
            $postData['post_category'] = [$categoryId];
        }

        $newId = wp_insert_post($postData);
        if (is_wp_error($newId)) {
            return ToolResult::error('文章创建失败: ' . $newId->get_error_message());
        }

        // 插入插图
        $coverImageId = 0;
        if ($illustrationCount > 0) {
            [$blocks, $coverImageId] = ScheduleWriteService::insertIllustrations(
                $newId, $blocks, $illustrationCount, $imageSource, $title
            );
        }

        // 设置封面图
        if ($generateCover && $coverImageId > 0) {
            set_post_thumbnail($newId, $coverImageId);
            update_post_meta($newId, '_xzd_ai_cover_source', 'inline_illustration');
        } elseif ($generateCover) {
            // 无插图时，尝试单独生成封面
            try {
                $imgClient = \XZD\AI\V5\AI\ImageClient::instance();
                // V3.1.7: 修正参数传递——generateCoverImage 签名为 (int $postId, array $options)
                $coverResult = $imgClient->generateCoverImage($newId, [
                    'title'   => $title,
                    'content' => $plainContent,
                    'source'  => $imageSource,
                    'style'   => 'realistic',
                ]);
                if (!empty($coverResult['attachment_id'])) {
                    update_post_meta($newId, '_xzd_ai_cover_source', $coverResult['source'] ?? 'generated');
                }
            } catch (\Throwable $e) {
                Logger::warning('write_article', '封面生成失败: ' . $e->getMessage());
            }
        }

        // 序列化最终内容并更新文章
        $postContent = GutenbergBlockBuilder::serialize($blocks);
        wp_update_post([
            'ID'           => $newId,
            'post_content' => $postContent,
        ]);

        // 设置最终发布状态
        if ($status === 'publish') {
            wp_update_post([
                'ID'          => $newId,
                'post_status' => 'publish',
            ]);
        } elseif ($status === 'pending') {
            wp_update_post([
                'ID'          => $newId,
                'post_status' => 'pending',
            ]);
        }

        // 设置标签
        if (!empty($tags)) {
            $tagNames = array_map('trim', explode(',', $tags));
            $tagNames = array_filter($tagNames);
            if (!empty($tagNames)) {
                wp_set_post_tags($newId, $tagNames, false);
            }
        }

        // 记录 meta
        update_post_meta($newId, '_xzd_ai_written', current_time('mysql'));
        update_post_meta($newId, '_xzd_ai_generated', true);

        // 递增日计数
        ScheduleWriteService::incrementTodayCount();

        // 异步生成 SEO（不阻塞文章创建）
        if ($generateSeo) {
            try {
                QueueManager::enqueue($newId, [], 'schedule_write');
            } catch (\Throwable $e) {
                Logger::warning('write_article', 'SEO调度失败: ' . $e->getMessage());
            }
        }

        return ToolResult::success([
            'post_id'        => $newId,
            'title'          => $title,
            'url'            => get_permalink($newId),
            'blocks'         => count($blocks),
            'illustrations'  => $illustrationCount > 0 ? true : false,
            'cover_image_id' => $coverImageId,
        ], "文章创建成功（" . count($blocks) . "个块）", [
            'tokens_used' => ($result['input_tokens'] ?? 0) + ($result['output_tokens'] ?? 0),
            'undo_data'   => ['post_id' => $newId],
        ]);
    }

    /**
     * 旧版写文章（降级方案 — 非结构化AI输出 → 段落块包装）
     */
    private static function writeArticleLegacy(array $params, int $userId): ToolResult
    {
        $topic = $params['topic'] ?? '';
        $keywords = $params['keywords'] ?? '';
        $categoryId = (int) ($params['category_id'] ?? 0);
        $status = $params['post_status'] ?? 'publish';

        $prompt = "写一篇关于\"{$topic}\"的文章";
        if ($keywords) $prompt .= "，包含以下关键词：{$keywords}";
        $prompt .= "\n重要：必须使用纯HTML格式(h2/h3/p/ul/ol/blockquote/pre/code/strong/em/a标签)，绝对不要使用Markdown(#/**/-)。文章结构：<h2>标题</h2><p>段落...</p>。800-2000字。";

        $result = Client::instance()->chat(
            [Types::user($prompt)],
            ['max_tokens' => 4000]
        );

        if (isset($result['error'])) return ToolResult::error($result['error']);

        $rawContent = $result['content'];
        $title = $topic;
        if (preg_match('/<h[23][^>]*>(.+?)<\/h[23]>/i', $rawContent, $m)) {
            $title = trim(strip_tags($m[1]));
        }

        // V3.1.7: 优先检测 Markdown 格式，强制转换为 Gutenberg 块
        $hasMarkdown = preg_match('/^#{1,6}\s|^\*\*|^-\s|^```|^\d+\.\s/m', $rawContent);
        $hasHtml = preg_match('/<\/?(?:h[1-6]|p|pre|ul|ol|blockquote|table)[^>]*>/i', $rawContent);

        // 如果是 Markdown(无论有无HTML)，优先使用 Markdown 解析器确保格式正确
        if ($hasMarkdown) {
            $blocks = self::parseMarkdownToBlocks($rawContent, $title);
        } else {
            $blocks = [];
            $parts = preg_split('/(<(?:h[1-6]|p|pre|ul|ol|blockquote|table)[^>]*>.*?<\/(?:h[1-6]|p|pre|ul|ol|blockquote|table)>)/is', $rawContent, -1, PREG_SPLIT_DELIM_CAPTURE);
            foreach ($parts as $part) {
                $part = trim($part);
                if (empty($part)) continue;
                if (preg_match('/^<h([1-6])[^>]*>(.+?)<\/h\1>$/is', $part, $m)) {
                    $blocks[] = GutenbergBlockBuilder::heading(trim(strip_tags($m[2])), (int) $m[1]);
                } elseif (preg_match('/^<pre[^>]*><code[^>]*>(.+?)<\/code><\/pre>$/is', $part, $m)) {
                    $blocks[] = GutenbergBlockBuilder::code(html_entity_decode($m[1]));
                } elseif (preg_match('/^<p[^>]*>(.+?)<\/p>$/is', $part, $m)) {
                    $blocks[] = GutenbergBlockBuilder::paragraph(trim(strip_tags($m[1], '<strong><em><code><a>')));
                } else {
                    $blocks[] = GutenbergBlockBuilder::paragraph($part);
                }
            }
        }

        $postContent = GutenbergBlockBuilder::serialize($blocks);

        $postData = [
            'post_title'   => $title,
            'post_content' => $postContent,
            'post_status'  => $status,
            'post_type'    => 'post',
            'post_author'  => $userId,
        ];
        if ($categoryId) $postData['post_category'] = [$categoryId];

        $newId = wp_insert_post($postData);
        if (is_wp_error($newId)) {
            return ToolResult::error('文章创建失败: ' . $newId->get_error_message());
        }

        update_post_meta($newId, '_xzd_ai_written', current_time('mysql'));

        return ToolResult::success(['post_id' => $newId, 'title' => $title, 'url' => get_permalink($newId)], '文章创建成功（降级模式）', [
            'tokens_used' => $result['input_tokens'] + $result['output_tokens'],
            'undo_data' => ['post_id' => $newId],
        ]);
    }

    /**
     * 将 Markdown 文本转换为 Gutenberg 块数组
     * 支持: #/##/### 标题, **粗体**, *斜体*, - 列表, ```代码块```, 普通段落
     */
    private static function parseMarkdownToBlocks(string $text, string &$title = ''): array
    {
        $blocks = [];
        $lines = explode("\n", $text);
        $buffer = '';
        $inCode = false;
        $codeLang = '';
        $codeBuffer = '';
        $inList = false;
        $listItems = [];

        foreach ($lines as $line) {
            // 代码块开始/结束
            if (preg_match('/^```(\w*)\s*$/', $line, $m)) {
                if (!$inCode) {
                    if (!empty($buffer)) {
                        $blocks[] = self::mdParagraphBlock($buffer);
                        $buffer = '';
                    }
                    $inCode = true;
                    $codeLang = $m[1] ?: '';
                    $codeBuffer = '';
                } else {
                    $blocks[] = GutenbergBlockBuilder::code(htmlspecialchars($codeBuffer), $codeLang);
                    $inCode = false;
                }
                continue;
            }

            if ($inCode) {
                $codeBuffer .= ($codeBuffer ? "\n" : '') . $line;
                continue;
            }

            // 标题 # ## ###
            if (preg_match('/^(#{1,6})\s+(.+)$/', $line, $m)) {
                if (!empty($buffer)) {
                    $blocks[] = self::mdParagraphBlock($buffer);
                    $buffer = '';
                }
                $level = strlen($m[1]);
                $headingText = trim(strip_tags($m[2]));
                $blocks[] = GutenbergBlockBuilder::heading($headingText, $level);
                if (empty($title) && $level <= 2) {
                    $title = $headingText;
                }
                continue;
            }

            // 无序列表项
            if (preg_match('/^[-*+]\s+(.+)$/', $line, $m)) {
                if (!empty($buffer)) {
                    $blocks[] = self::mdParagraphBlock($buffer);
                    $buffer = '';
                }
                $listItems[] = '<li>' . self::mdInlineFormat(trim($m[1])) . '</li>';
                $inList = true;
                continue;
            }

            // 列表结束
            if ($inList && !preg_match('/^[-*+]\s+/', $line)) {
                $blocks[] = ['blockName' => 'core/list', 'attrs' => [], 'innerBlocks' => [], 'innerHTML' => '<ul>' . implode('', $listItems) . '</ul>', 'innerContent' => ['<ul>' . implode('', $listItems) . '</ul>']];
                $listItems = [];
                $inList = false;
            }

            // 空行 → 段落分隔
            if (trim($line) === '') {
                if (!empty($buffer)) {
                    $blocks[] = self::mdParagraphBlock($buffer);
                    $buffer = '';
                }
                continue;
            }

            // 普通文本 → 累积到缓冲区
            $buffer .= ($buffer ? "\n" : '') . $line;
        }

        // 收尾
        if ($inList && !empty($listItems)) {
            $blocks[] = ['blockName' => 'core/list', 'attrs' => [], 'innerBlocks' => [], 'innerHTML' => '<ul>' . implode('', $listItems) . '</ul>', 'innerContent' => ['<ul>' . implode('', $listItems) . '</ul>']];
        }
        if (!empty($buffer)) {
            $blocks[] = self::mdParagraphBlock($buffer);
        }
        if ($inCode && !empty($codeBuffer)) {
            $blocks[] = GutenbergBlockBuilder::code(htmlspecialchars($codeBuffer), $codeLang);
        }

        return $blocks;
    }

    private static function mdParagraphBlock(string $text): array
    {
        return GutenbergBlockBuilder::paragraph(self::mdInlineFormat(trim($text)));
    }

    private static function mdInlineFormat(string $text): string
    {
        $text = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $text);
        $text = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $text);
        $text = preg_replace('/`([^`]+)`/', '<code>$1</code>', $text);
        return $text;
    }

    /**
     * 词库匹配 — 返回匹配到的标签数组（非 ToolResult），供 hybrid 模式复用
     *
     * ★ V3.1.7+ 真匹配修复：使用词边界正则替代 mb_stripos 模糊子串匹配
     *   - 中文：在标签前后检查中文字符边界
     *   - 英文/数字：用 \b 词边界
     *   - 避免短词误匹配（如 "AI" 匹配到 "rain"、"Go" 匹配到 "good"）
     */
    private static function matchFromLibraryRaw(string $content, int $postId, int $limit): array
    {
        global $wpdb;
        // ★ 直接使用 WordPress 系统标签库（wp_terms + wp_term_taxonomy）
        // 按使用次数降序 + 标签名长度降序：热门长标签优先匹配，短标签自然被长标签重叠保护
        $tags = $wpdb->get_results(
            "SELECT t.name AS tag_name, tt.count AS post_count
             FROM {$wpdb->terms} t
             JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
             WHERE tt.taxonomy = 'post_tag' AND tt.count > 0
             ORDER BY CHAR_LENGTH(t.name) DESC, tt.count DESC
             LIMIT 200",
            ARRAY_A
        );

        if (empty($tags)) {
            return [];
        }

        $matched = [];
        $matchedPositions = []; // 记录已匹配位置，防止短标签重叠

        foreach ($tags as $tag) {
            if (count($matched) >= $limit) break;
            $name = $tag['tag_name'];

            // 找到内容中所有出现位置
            $pos = self::findIndependentTag($name, $content, $matchedPositions);
            if ($pos !== false) {
                $matched[] = ['name' => $name, 'confidence' => 80];
                $byteLen = strlen($name);
                // 记录该标签在内容中的字节范围，防止短词重叠匹配
                $matchedPositions[] = [$pos, $pos + $byteLen];
            }
        }

        return $matched;
    }

    /**
     * 在内容中查找独立标签 — 结合词边界 + 已匹配位置去重
     *
     * @param array $occupied 已匹配位置 [[start, end], ...]
     * @return int|false 匹配位置（字节偏移），未找到返回 false
     */
    private static function findIndependentTag(string $tagName, string $content, array $occupied): int|false
    {
        $tagLen = mb_strlen($tagName);
        if ($tagLen < 2) return false;

        $escaped = preg_quote($tagName, '/');
        $hasChinese = (bool) preg_match('/[\x{4e00}-\x{9fff}]/u', $tagName);

        // 构建正则
        if ($hasChinese) {
            // 中文标签：简单子串匹配 + 长标签优先去重
            // 中文无空格分隔，正则词边界对句中词不适用；靠长标签优先匹配后
            // 短标签若与已匹配长标签重叠即跳过，比正则边界更准确
            $bytePos = strpos($content, $tagName);
            if ($bytePos === false) return false;
            $byteEnd = $bytePos + strlen($tagName);
            foreach ($occupied as [$occStart, $occEnd]) {
                if (!($byteEnd <= $occStart || $bytePos >= $occEnd)) {
                    return false; // 与已匹配长标签重叠
                }
            }
            return $bytePos;
        } else {
            $chineseBoundary = '\x{4e00}-\x{9fff}\x{3000}-\x{303f}\x{ff00}-\x{ffef}';
            $pattern = '/(?:\b|(?<=[' . $chineseBoundary . '\s]))' . $escaped . '(?:\b|(?=[' . $chineseBoundary . '\s]))/ui';
        }

        // 找到所有匹配位置，返回第一个未被占用的
        if (!preg_match_all($pattern, $content, $matches, PREG_OFFSET_CAPTURE)) {
            return false;
        }

        foreach ($matches[0] as $m) {
            $start = $m[1];
            $end = $start + strlen($m[0]);

            // 检查是否与已匹配标签重叠
            $overlap = false;
            foreach ($occupied as [$occStart, $occEnd]) {
                // 使用字符位置而非字节位置做近似判断
                if (!($end <= $occStart || $start >= $occEnd)) {
                    $overlap = true;
                    break;
                }
            }
            if (!$overlap) {
                return $start;
            }
        }

        return false;
    }

    /**
     * 真匹配检查 — 标签是否以独立词汇形式出现在内容中
     *
     * ★ 中文/英文采用不同的词边界策略：
     *   - 中文标签：前后不能紧邻中文字符（防止"学习"匹配到"机器学习"）
     *   - 英文标签：英文词边界 \b 有效，中文包围也视为独立词
     */
    private static function isTagInContent(string $tagName, string $content): bool
    {
        if (mb_strlen($tagName) < 2) {
            return false; // 单字符太容易误匹配，拒绝
        }

        $escaped = preg_quote($tagName, '/');
        $hasChinese = (bool) preg_match('/[\x{4e00}-\x{9fff}]/u', $tagName);

        if ($hasChinese) {
            // 中文标签：简单子串匹配 — 词边界由 matchFromLibraryRaw 的长标签优先排序保证
            // 单独使用时（如旧 matchFromLibrary）效果等同于原 mb_stripos，但仅匹配不含标签名的情况极少
            return mb_strpos($content, $tagName) !== false;
        }

        // 英文/数字标签：\b 词边界（大小写不敏感）或 被中文/标点包围（中文天然隔离英文词）
        // 中文标点范围：\x{3000}-\x{303f} CJK符号  \x{ff00}-\x{ffef} 全角标点
        $chineseBoundary = '\x{4e00}-\x{9fff}\x{3000}-\x{303f}\x{ff00}-\x{ffef}';
        return (bool) preg_match(
            '/(?:\b|(?<=[' . $chineseBoundary . '\s]))' .
            $escaped .
            '(?:\b|(?=[' . $chineseBoundary . '\s]))/ui',
            $content
        );
    }

    /**
     * 词库匹配 — 返回 ToolResult（library 模式直接使用）
     * @deprecated 保留向后兼容，内部调用 matchFromLibraryRaw
     */
    private static function matchFromLibrary(string $content, int $postId, int $count): ToolResult
    {
        global $wpdb;
        // 检查系统是否有标签
        $exists = $wpdb->get_var(
            "SELECT COUNT(*) FROM {$wpdb->terms} t
             JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
             WHERE tt.taxonomy = 'post_tag' AND tt.count > 0"
        );
        if (!$exists) {
            return ToolResult::error('系统标签库为空，请先手动创建标签或使用 AI 模式生成');
        }

        $matched = self::matchFromLibraryRaw($content, $postId, $count);

        foreach ($matched as $tag) {
            wp_set_post_tags($postId, [$tag['name']], true);
        }

        return ToolResult::success(
            ['tags' => $matched, 'count' => count($matched)],
            "词库匹配 " . count($matched) . " 个标签"
        );
    }

    /**
     * 单段落文本处理 — 用于编辑器块工具栏✨Popover
     *
     * 不写数据库，只返回处理后的文本，前端直接替换块内容。
     * action: polish | rewrite | expand | shorten | translate_en | translate_ja | fixGrammar
     */
    public static function processText(array $params, int $userId): ToolResult
    {
        $content = $params['content'] ?? '';
        $action  = $params['action'] ?? 'polish';

        if (empty(trim($content))) {
            return ToolResult::error('请提供要处理的文本内容');
        }

        $protected = self::protectBlocks($content);
        $textToProcess = $protected['content'];

        $actionPrompts = [
            'polish'       => "请润色以下文本，优化表达、提升可读性和流畅度。保持原意不变，直接返回润色后的纯文本：\n\n",
            'rewrite'      => "请用不同的表达方式改写以下文本，换一种风格但保持原意。直接返回改写后的纯文本：\n\n",
            'expand'       => "请扩展以下文本，增加细节和示例，使内容更加充实丰富（约1.5-2倍长度）。直接返回扩展后的文本：\n\n",
            'shorten'      => "请精简以下文本，删除冗余表达，保留核心信息（约0.5-0.7倍长度）。直接返回精简后的纯文本：\n\n",
            'translate_en' => "请将以下文本翻译为流利的英文。直接返回翻译结果：\n\n",
            'translate_ja' => "请将以下文本翻译为自然的日文。直接返回翻译结果：\n\n",
            'fixGrammar'   => "请修正以下文本中的语法错误、错别字和标点符号问题。仅修正错误，不要改写内容。直接返回修正后的纯文本：\n\n",
        ];

        $prompt = ($actionPrompts[$action] ?? $actionPrompts['polish']) . $textToProcess;

        $result = Client::instance()->chat(
            [Types::user($prompt)],
            ['max_tokens' => 4000]
        );

        if (isset($result['error'])) {
            return ToolResult::error($result['error']);
        }

        $processed = trim($result['content']);
        $processed = preg_replace('/^```[a-z]*\s*/i', '', $processed);
        $processed = preg_replace('/\s*```$/', '', $processed);

        $processed = self::restoreBlocks($processed, $protected['blocks']);

        return ToolResult::success([
            'content' => $processed,
            'action'  => $action,
        ], '文本处理完成', [
            'tokens_used' => $result['input_tokens'] + $result['output_tokens'],
        ]);
    }

    /**
     * 快速插入 — 根据描述生成Gutenberg块
     *
     * block_type: code | feature | alert | quote
     * 返回序列化后的块HTML，前端直接 insertBlocks
     */
    public static function generateBlock(array $params, int $userId): ToolResult
    {
        $blockType = $params['block_type'] ?? 'code';
        $prompt    = $params['prompt'] ?? '';

        if (empty(trim($prompt))) {
            return ToolResult::error('请描述你需要生成的内容');
        }

        return match ($blockType) {
            'code'    => self::generateCodeBlock($prompt),
            'feature' => self::generateFeatureCards($prompt),
            'alert'   => self::generateAlertBlock($prompt),
            'quote'   => self::generateQuoteBlock($prompt),
            default   => ToolResult::error("不支持的块类型: {$blockType}"),
        };
    }

    /** P1-6: 缩短内容包装器 */
    public static function shortenContent(array $params, int $userId): ToolResult
    {
        $params['action'] = 'shorten';
        return self::processText($params, $userId);
    }

    /** P1-6: 语法修正包装器 */
    public static function fixGrammar(array $params, int $userId): ToolResult
    {
        $params['action'] = 'fixGrammar';
        return self::processText($params, $userId);
    }

    /**
     * AI生成代码块 — 输入需求描述，返回带语法高亮的代码
     */
    private static function generateCodeBlock(string $prompt): ToolResult
    {
        $result = Client::instance()->chatStructured(
            [Types::user("根据以下描述生成代码。选择合适的编程语言。\n\n{$prompt}")],
            [
                'type' => 'object',
                'properties' => [
                    'language' => ['type' => 'string', 'description' => '编程语言，如javascript/python/go'],
                    'title'    => ['type' => 'string', 'description' => '代码块标题'],
                    'code'     => ['type' => 'string', 'description' => '完整的代码内容'],
                ],
                'required' => ['language', 'code'],
            ],
            ['max_tokens' => 3000]
        );

        if (isset($result['error'])) {
            return ToolResult::error($result['error']);
        }

        $data = $result['content'];
        $block = GutenbergBlockBuilder::codeHighlight(
            $data['code'] ?? '',
            $data['language'] ?? 'generic',
            $data['title'] ?? '',
            true
        );
        $html = GutenbergBlockBuilder::serialize([$block]);

        return ToolResult::success([
            'block_html' => $html,
            'block_type' => 'code',
            'language'   => $data['language'] ?? 'generic',
        ], '代码块已生成', ['tokens_used' => $result['input_tokens'] + $result['output_tokens']]);
    }

    /**
     * AI生成亮点卡片×4 — 输入主题，返回4个图标+标题+描述的特征卡片
     */
    private static function generateFeatureCards(string $prompt): ToolResult
    {
        $result = Client::instance()->chatStructured(
            [Types::user("为主题「{$prompt}」生成4个亮点特征卡片。每个卡片包含FontAwesome图标名（如fa-bolt）、标题、简短描述。返回JSON数组。")],
            [
                'type' => 'object',
                'properties' => [
                    'title'  => ['type' => 'string', 'description' => '亮点区域大标题'],
                    'cards'  => [
                        'type' => 'array',
                        'minItems' => 3,
                        'maxItems' => 5,
                        'items' => [
                            'type' => 'object',
                            'properties' => [
                                'icon'  => ['type' => 'string', 'description' => 'FontAwesome图标名，如 fa-bolt'],
                                'title' => ['type' => 'string', 'description' => '卡片标题'],
                                'note'  => ['type' => 'string', 'description' => '卡片描述'],
                            ],
                            'required' => ['icon', 'title', 'note'],
                        ],
                    ],
                ],
                'required' => ['cards'],
            ],
            ['max_tokens' => 2000]
        );

        if (isset($result['error'])) {
            return ToolResult::error($result['error']);
        }

        $data = $result['content'];
        $cards = $data['cards'] ?? [];

        $blocks = [];
        // 添加区域标题
        if (!empty($data['title'])) {
            $blocks[] = GutenbergBlockBuilder::styledHeading($data['title'], '#1fbc45', 'h3');
        }

        foreach ($cards as $card) {
            $blocks[] = GutenbergBlockBuilder::featureCard(
                $card['title'] ?? '',
                $card['note'] ?? '',
                $card['icon'] ?? 'fa-star',
                ''
            );
        }

        $html = GutenbergBlockBuilder::serialize($blocks);

        return ToolResult::success([
            'block_html' => $html,
            'block_type' => 'feature',
            'count'      => count($cards),
        ], "已生成 " . count($cards) . " 个亮点卡片", ['tokens_used' => $result['input_tokens'] + $result['output_tokens']]);
    }

    /**
     * AI生成提醒框 — 输入内容提示，返回样式化的alert块
     */
    private static function generateAlertBlock(string $prompt): ToolResult
    {
        $result = Client::instance()->chatStructured(
            [Types::user("基于以下提示生成一个醒目提醒框。选择合适的样式（info/warning/success/danger）。\n\n{$prompt}")],
            [
                'type' => 'object',
                'properties' => [
                    'style'   => ['type' => 'string', 'enum' => ['info', 'warning', 'success', 'danger'], 'description' => '提醒框样式'],
                    'content' => ['type' => 'string', 'description' => '提醒框文字内容'],
                ],
                'required' => ['style', 'content'],
            ],
            ['max_tokens' => 1000]
        );

        if (isset($result['error'])) {
            return ToolResult::error($result['error']);
        }

        $data = $result['content'];
        $block = GutenbergBlockBuilder::alert(
            $data['content'] ?? '',
            $data['style'] ?? 'info',
            false
        );
        $html = GutenbergBlockBuilder::serialize([$block]);

        return ToolResult::success([
            'block_html' => $html,
            'block_type' => 'alert',
            'style'      => $data['style'] ?? 'info',
        ], '提醒框已生成', ['tokens_used' => $result['input_tokens'] + $result['output_tokens']]);
    }

    /**
     * AI生成引用块
     */
    private static function generateQuoteBlock(string $prompt): ToolResult
    {
        $result = Client::instance()->chatStructured(
            [Types::user("基于以下主题生成一句精彩的引用/格言式内容。\n\n{$prompt}")],
            [
                'type' => 'object',
                'properties' => [
                    'content'  => ['type' => 'string', 'description' => '引用文字内容'],
                    'citation' => ['type' => 'string', 'description' => '引用来源（可选）'],
                ],
                'required' => ['content'],
            ],
            ['max_tokens' => 500]
        );

        if (isset($result['error'])) {
            return ToolResult::error($result['error']);
        }

        $data = $result['content'];
        $block = GutenbergBlockBuilder::styledQuote(
            $data['content'] ?? '',
            ''
        );
        $html = GutenbergBlockBuilder::serialize([$block]);

        return ToolResult::success([
            'block_html' => $html,
            'block_type' => 'quote',
        ], '引用块已生成', ['tokens_used' => $result['input_tokens'] + $result['output_tokens']]);
    }
}
