<?php

namespace XZD\AI\V5\Tools;

use XZD\AI\V5\AI\Client;
use XZD\AI\V5\AI\Types;
use XZD\AI\V5\Core\Config;
use XZD\AI\V5\Core\Logger;

/**
 * 融合流水线 - 一次AI调用完成：润色(含内链嵌入) + 标签 + SEO + 摘要
 *
 * 内链使用 <a data-xzd="搜索词">锚文本</a> 格式（AI熟悉的HTML标签）
 * 后端用DOM/正则提取后搜索真实文章，替换为带样式的<a>标签
 */
class MergedPipeline
{
    const MERGEABLE_AI_STAGES = [
        'generate_tags', 'generate_seo', 'generate_summary',
        'build_autolink', 'polish_content',
    ];

    const SKIP_AI_MODES = [
        'generate_tags'  => ['library'],
        'build_autolink' => ['keyword', 'category'],
    ];

    const IMAGE_STAGES   = ['generate_cover', 'generate_alt'];
    const NON_AI_STAGES  = [
        'push_search', 'generate_schema', 'score_seo',
        'localize_image', 'compress_image', 'apply_watermark', 'convert_to_webp',
    ];

    // 匹配AI嵌入的内链标记：<a data-xzd="搜索词">锚文本</a>
    const LINK_PATTERN = '/<a\s+data-xzd="([^"]*)"\s*>(.*?)<\/a>/u';

    // 主入口

    /** @param array $enabledStages 每项: ['id' => string, 'params' => array] */
    public function run(int $postId, array $enabledStages): array
    {
        $post = get_post($postId);
        if (!$post) return ['error' => '文章不存在'];

        $aiStages    = [];
        $skipStages  = [];
        $imageStages = [];
        $nonAiStages = [];

        foreach ($enabledStages as $stage) {
            $id     = $stage['id'] ?? '';
            $params = $stage['params'] ?? [];

            if (in_array($id, self::IMAGE_STAGES, true))  { $imageStages[] = $stage; continue; }
            if (in_array($id, self::NON_AI_STAGES, true)) { $nonAiStages[] = $stage; continue; }
            if (!in_array($id, self::MERGEABLE_AI_STAGES, true)) { $nonAiStages[] = $stage; continue; }

            $skipModes    = self::SKIP_AI_MODES[$id] ?? [];
            $currentMode  = $params['mode'] ?? '';
            if (!empty($skipModes) && in_array($currentMode, $skipModes, true)) {
                $skipStages[] = $stage; continue;
            }

            if ($this->shouldSkipByProcessingMode($postId, $id, $params)) {
                $skipStages[] = $stage; continue;
            }

            $aiStages[] = $stage;
        }

        $results = ['merged_ai' => [], 'skipped' => [], 'image' => [], 'non_ai' => []];

        if (!empty($aiStages)) {
            try {
                $results['merged_ai'] = $this->executeMergedAI($postId, $post, $aiStages);
            } catch (\Throwable $e) {
                Logger::error('merged_pipeline', '融合AI调用失败: ' . $e->getMessage(), [
                    'post_id' => $postId, 'stages' => array_column($aiStages, 'id'),
                ]);
                $results['merged_ai'] = $this->fallbackIndividual($postId, $aiStages);
                $results['_fallback'] = true;
            }
        }

        foreach ($skipStages as $stage)  { $results['skipped'][] = $this->executeIndependent($postId, $stage); }
        foreach ($imageStages as $stage) { $results['image'][]   = $this->scheduleImageStage($postId, $stage); }
        foreach ($nonAiStages as $stage) { $results['non_ai'][]  = $this->executeIndependent($postId, $stage); }

        return $results;
    }

    // 融合AI调用

    private function executeMergedAI(int $postId, \WP_Post $post, array $stages): array
    {
        $hasPolish   = $this->hasStage($stages, 'polish_content');
        $hasAutolink = $this->hasStage($stages, 'build_autolink');

        // 保护HTML媒体元素不被AI修改
        $rawHtml      = $post->post_content;
        $protected    = $this->protectBlocks($rawHtml);
        $content      = $protected['content'];
        $placeholders = $protected['blocks'];

        $plainContent = wp_strip_all_tags(strip_shortcodes($rawHtml));
        $content = mb_substr($content, 0, 8000);

        $systemPrompt = $this->buildSystemPrompt($stages, $hasPolish, $hasAutolink);
        $userPrompt   = $this->buildUserPrompt($post, $content, $stages, $hasPolish, $hasAutolink);
        $jsonSchema   = $this->buildJsonSchema($stages, $hasAutolink);
        $model        = $this->pickModel($stages);

        // 标签：词库 + 标签池注入
        $libraryTags = [];
        $tagsStage = $this->findStage($stages, 'generate_tags');
        if ($tagsStage) {
            $tagMode = $tagsStage['params']['mode'] ?? 'ai';
            $tagCount = (int)($tagsStage['params']['count'] ?? 5);

            // hybrid / library 模式：词库先匹配
            if ($tagMode === 'hybrid' || $tagMode === 'library') {
                $libraryTags = ContentManager::matchFromLibraryRaw($plainContent, $postId, $tagCount);
            }

            // 注入标签池（所有模式），让AI优先复用已有标签
            $tagPool = self::getTopTags(30); // 融合模式用30个防止prompt过长
            if (!empty($tagPool)) {
                $poolNames = array_column($tagPool, 'name');
                $userPrompt .= "\n\n站点标签库（优先从中选择）：" . implode('、', $poolNames);
            }

            if (!empty($libraryTags)) {
                $libNames = array_column($libraryTags, 'name');
                $userPrompt .= "\n已知标签（勿重复）：" . implode('、', $libNames);
                $remaining = max(1, $tagCount - count($libraryTags));
                $userPrompt .= "\n请从标签库中补充 {$remaining} 个标签（如确实需要可补充最多1个新标签）";
            }
        }

        $stageNames = implode(',', array_column($stages, 'id'));
        Logger::info('merged_pipeline', "融合: post={$postId}, stages={$stageNames}, polish={$hasPolish}, autolink={$hasAutolink}, model=" . ($model ?: 'default'));

        $options = ['max_tokens' => $hasPolish ? 8000 : 3000, 'disable_thinking' => true];
        if ($model) $options['model'] = $model;

        $aiResult = Client::instance()->chatStructured(
            [Types::system($systemPrompt), Types::user($userPrompt)],
            $jsonSchema,
            $options
        );

        if (isset($aiResult['error'])) {
            throw new \RuntimeException('融合AI调用失败: ' . $aiResult['error']);
        }

        $data = $aiResult['content'] ?? [];
        if (!is_array($data)) {
            throw new \RuntimeException('AI返回非结构化数据');
        }

        return $this->dispatchResults($postId, $post, $data, $stages, $hasPolish, $hasAutolink, $libraryTags, $placeholders);
    }

    // 系统提示词

    private function buildSystemPrompt(array $stages, bool $hasPolish, bool $hasAutolink): string
    {
        $polishStage = $this->findStage($stages, 'polish_content');
        $depth       = $polishStage['params']['depth'] ?? 'medium';
        $guideline   = $polishStage['params']['guideline'] ?? '';

        $depthMap = [
            'light'  => '轻度优化：修正错别字和语病，调整不通顺的句子，保持原文风格',
            'medium' => '中度优化：优化表达方式，提升可读性和逻辑性，调整段落结构',
            'deep'   => '深度优化：全面改写，使用更生动有力的表达，重新组织文章结构',
        ];
        $depthDesc = $depthMap[$depth] ?? $depthMap['medium'];

        $lines = [];
        $lines[] = '你是一位资深内容编辑和SEO专家。';

        // 内链指令放最前面
        if ($hasPolish && $hasAutolink) {
            $al = $this->findStage($stages, 'build_autolink');
            $n  = (int)($al['params']['count'] ?? 3);
            $lines[] = "[最高优先级] 润色时，把文中 {$n} 个关键词用此格式标记：<a data-xzd=\"搜索词\">关键词</a>";
            $lines[] = '分散在不同段落中。禁止使用任何其他格式。禁止在文末添加"相关阅读"区块。';
            $lines[] = '';
        }

        $lines[] = '## 核心准则';

        if ($hasPolish) {
            $lines[] = "### 润色（{$depth}）";
            $lines[] = $depthDesc;
            if ($guideline) $lines[] = "要求：{$guideline}";
            $lines[] = '';
        }

        $lines[] = '### 去AI味（重要）';
        $lines[] = '禁止使用：在当今时代、随着...的发展、众所周知、综上所述、总而言之、此外、值得一提的是';
        $lines[] = '用口语化短句，像真人写的。直接切入主题，不写空洞开头。多用具体细节，少用抽象概括。';
        $lines[] = '';

        $lines[] = '### SEO优化';
        $lines[] = '关键词自然出现在标题和首段。段落层次清晰（h2/h3）。信息密度高，每句话有价值。';
        $lines[] = '';

        if ($hasPolish && $hasAutolink) {
            $lines[] = '### 内链提醒';
            $lines[] = '上文已要求嵌入 <a data-xzd="搜索词">关键词</a> 标记。务必执行。';
            $lines[] = '';
        }

        $lines[] = '### 占位符保护';
        $lines[] = '%%IMG_X%%、%%CODE_X%% 等是系统占位符，原封不动保留。';
        $lines[] = '';
        $lines[] = '只输出JSON，不要多余文字。';

        return implode("\n", $lines);
    }

    private function buildUserPrompt(\WP_Post $post, string $content, array $stages, bool $hasPolish, bool $hasAutolink): string
    {
        $lines   = [];
        $lines[] = "## 文章标题\n{$post->post_title}";
        $lines[] = "\n## 文章内容\n{$content}";
        $lines[] = "\n---\n## 任务";

        $step = 1;

        if ($hasPolish) {
            $ps = $this->findStage($stages, 'polish_content');
            $d  = $ps['params']['depth'] ?? 'medium';
            $g  = $ps['params']['guideline'] ?? '';
            $lines[] = "{$step}. 按「{$d}」深度润色全文" . ($g ? "（方向：{$g}）" : '') . "。去AI味、增强可读性。";
            if ($hasAutolink) {
                $al = $this->findStage($stages, 'build_autolink');
                $n  = (int)($al['params']['count'] ?? 3);
                $lines[] = "   嵌入 {$n} 个内链标记：<a data-xzd=\"搜索词\">关键词</a>";
                $lines[] = "   示例：如果文中有'云服务器'这个词，写成 <a data-xzd=\"云服务器\">云服务器</a>";
                $lines[] = "   分散在不同段落。禁止放在文末。";
            }
            $step++;
        }

        foreach ($stages as $stage) {
            $id     = $stage['id'];
            $params = $stage['params'] ?? [];
            if ($id === 'polish_content') continue;
            if ($id === 'build_autolink' && $hasPolish) continue;

            switch ($id) {
                case 'generate_tags':
                    $c = (int)($params['count'] ?? 5);
                    $conf = (int)($params['confidence'] ?? 60);
                    $lines[] = "{$step}. 从标签库中选择 {$c} 个最相关标签（置信度>={$conf}%）。如确实需要可补充最多1个新标签（通用主题词）";
                    break;
                case 'generate_seo':
                    $tl = (int)($params['titleLen'] ?? 60);
                    $dl = (int)($params['descLen'] ?? 160);
                    $lines[] = "{$step}. SEO：标题<={$tl}字（吸引点击+含关键词）、关键词（逗号分隔）、描述<={$dl}字";
                    break;
                case 'generate_summary':
                    $l = (int)($params['len'] ?? 100);
                    $s = $params['style'] ?? 'formal';
                    $sm = ['formal'=>'正式专业','casual'=>'轻松易懂','tech'=>'技术精准'];
                    $sd = $sm[$s] ?? '正式专业';
                    $lines[] = "{$step}. 摘要<={$l}字（{$sd}风格）";
                    break;
                case 'build_autolink':
                    $n = (int)($params['count'] ?? 3);
                    $lines[] = "{$step}. 推荐 {$n} 个内链（锚文本+搜索关键词）";
                    break;
            }
            $step++;
        }

        $lines[] = "\n## 输出要求";
        $lines[] = '- 所有数据基于' . ($hasPolish ? '润色后' : '原文') . '内容，确保匹配';

        if ($hasPolish && $hasAutolink) {
            $al = $this->findStage($stages, 'build_autolink');
            $n  = (int)($al['params']['count'] ?? 3);
            $lines[] = "- polished_content必须包含 {$n} 个 <a data-xzd=\"搜索词\">关键词</a> 标记（分散嵌入段落，非独立区块）";
        }

        return implode("\n", $lines);
    }

    // JSON Schema

    private function buildJsonSchema(array $stages, bool $hasAutolink): array
    {
        $properties = [];
        $required   = [];

        foreach ($stages as $stage) {
            switch ($stage['id']) {
                case 'polish_content':
                    $desc = '润色后的全文（保持HTML结构）';
                    if ($hasAutolink) {
                        $al = $this->findStage($stages, 'build_autolink');
                        $n  = (int)($al['params']['count'] ?? 3);
                        $desc .= "。文中已用 <a data-xzd=\"搜索词\">关键词</a> 格式嵌入{$n}个内链标记（分散在不同段落）";
                    }
                    $properties['polished_content'] = ['type' => 'string', 'description' => $desc];
                    $required[] = 'polished_content';
                    break;

                case 'generate_tags':
                    $properties['tags'] = [
                        'type' => 'array',
                        'items' => ['type' => 'object', 'properties' => [
                            'name' => ['type' => 'string'], 'confidence' => ['type' => 'integer'],
                        ], 'required' => ['name', 'confidence']],
                    ];
                    $required[] = 'tags';
                    break;

                case 'generate_seo':
                    $properties['seo_title']       = ['type' => 'string'];
                    $properties['seo_keywords']    = ['type' => 'string'];
                    $properties['seo_description'] = ['type' => 'string'];
                    $required[] = 'seo_title'; $required[] = 'seo_keywords'; $required[] = 'seo_description';
                    break;

                case 'generate_summary':
                    $properties['summary'] = ['type' => 'string'];
                    $required[] = 'summary';
                    break;

                case 'build_autolink':
                    if (!$hasAutolink) {
                        $properties['autolinks'] = [
                            'type' => 'array',
                            'items' => ['type' => 'object', 'properties' => [
                                'anchor_text' => ['type' => 'string'],
                                'keyword' => ['type' => 'string'],
                            ], 'required' => ['anchor_text', 'keyword']],
                        ];
                        $required[] = 'autolinks';
                    }
                    break;
            }
        }

        return ['type' => 'object', 'properties' => $properties, 'required' => $required];
    }

    // 结果分发

    private function dispatchResults(int $postId, \WP_Post $post, array $data, array $stages, bool $hasPolish, bool $hasAutolink, array $libraryTags = [], array $placeholders = []): array
    {
        $results = [];

        // 1. 润色 + 内链
        if ($hasPolish && isset($data['polished_content']) && is_string($data['polished_content'])) {
            $polishedContent = $data['polished_content'];

            if (!empty($placeholders)) {
                $polishedContent = $this->restoreBlocks($polishedContent, $placeholders);
            }

            // 切除"相关阅读"区块（兜底）
            $polishedContent = $this->stripRelatedReading($polishedContent);

            // 替换内链标记为真实链接
            $autolinkStage = $this->findStage($stages, 'build_autolink');
            $linkInserted  = 0;

            if ($hasAutolink && $autolinkStage && !empty(trim($polishedContent))) {
                $colors = [
                    'link_color'      => $autolinkStage['params']['link_color'] ?? '#2F6BFF',
                    'link_weight'     => $autolinkStage['params']['link_weight'] ?? '500',
                    'text_decoration' => $autolinkStage['params']['text_decoration'] ?? 'underline',
                ];
                $polishedContent = $this->replaceLinkMarkers($polishedContent, $postId, $colors, $linkInserted);
            }

            if (!empty(trim($polishedContent)) && $polishedContent !== $post->post_content) {
                $existingBackup = get_post_meta($postId, '_xzd_ai_content_backup', true);
                if (empty($existingBackup)) {
                    update_post_meta($postId, '_xzd_ai_content_backup', $post->post_content);
                }
                wp_update_post(['ID' => $postId, 'post_content' => $polishedContent]);
                update_post_meta($postId, '_xzd_ai_polished', current_time('mysql'));
                update_post_meta($postId, '_xzd_ai_processed_polish_content', json_encode([
                    'status' => 'completed', 'mode' => 'merged',
                    'timestamp' => current_time('mysql'),
                ]));
                $results['polish_content'] = [
                    'tool_id' => 'polish_content', 'success' => true,
                    'data' => ['polished' => true, 'autolinks_inserted' => $linkInserted],
                    'message' => "润色完成" . ($linkInserted > 0 ? "，嵌入 {$linkInserted} 个内链" : ''),
                ];
                Logger::info('merged_pipeline', "润色: post={$postId}, autolinks={$linkInserted}");
            } else {
                $results['polish_content'] = [
                    'tool_id' => 'polish_content', 'success' => true,
                    'data' => ['polished' => false], 'message' => '润色后无变化',
                ];
            }

            if ($hasAutolink && $linkInserted > 0) {
                update_post_meta($postId, '_xzd_ai_autolinks', current_time('mysql'));
                update_post_meta($postId, '_xzd_ai_processed_build_autolink', json_encode([
                    'status' => 'completed', 'mode' => 'merged',
                    'timestamp' => current_time('mysql'),
                    'summary' => "嵌入 {$linkInserted} 个内链",
                ]));
                $results['build_autolink'] = [
                    'tool_id' => 'build_autolink', 'success' => true,
                    'data' => ['inserted' => $linkInserted],
                    'message' => "内链嵌入：{$linkInserted} 个",
                ];
            }
        }

        // 2. 标签
        $tagsStage = $this->findStage($stages, 'generate_tags');
        if ($tagsStage && isset($data['tags']) && is_array($data['tags'])) {
            $procMode = $tagsStage['params']['processing_mode'] ?? 'fill_empty';
            $existingNames = [];
            if ($procMode === 'append') {
                $existing = wp_get_post_tags($postId, ['fields' => 'names']);
                $existingNames = is_array($existing) ? $existing : [];
            } elseif ($procMode === 'overwrite') {
                wp_set_post_tags($postId, []);
            }

            $aiNames = [];
            foreach ($data['tags'] as $tag) {
                if (!empty($tag['name'])) $aiNames[] = $tag['name'];
            }

            // 🔑 新标签相似度检查：AI返回的标签优先匹配已有标签
            $checkedNames = [];
            foreach ($aiNames as $name) {
                if (term_exists($name, 'post_tag')) {
                    $checkedNames[] = $name; // 已存在，直接使用
                } else {
                    $similar = self::findSimilarTag($name, 80);
                    if ($similar) {
                        Logger::info('tag_similarity', "融合流水线：标签 '{$name}' 合并到已有标签 '{$similar['name']}'", [
                            'post_id' => $postId, 'new_tag' => $name, 'existing_tag' => $similar['name'],
                        ]);
                        $checkedNames[] = $similar['name'];
                    } else {
                        // 无相似标签，创建新标签
                        $result = wp_insert_term($name, 'post_tag');
                        if (!is_wp_error($result)) {
                            $checkedNames[] = $name;
                        }
                    }
                }
            }

            $libNames = array_column($libraryTags, 'name');
            $allNames = array_values(array_unique(array_merge($libNames, $checkedNames, $existingNames)));
            $allNames = array_slice($allNames, 0, (int)($tagsStage['params']['count'] ?? 5));

            foreach ($allNames as $name) wp_set_post_tags($postId, [$name], true);

            update_post_meta($postId, '_xzd_ai_tags_generated', current_time('mysql'));
            update_post_meta($postId, '_xzd_ai_tags_count', count($allNames));
            update_post_meta($postId, '_xzd_ai_tags_list', $allNames);
            update_post_meta($postId, '_xzd_ai_tags_content_hash', md5(wp_strip_all_tags(strip_shortcodes($post->post_content))));
            update_post_meta($postId, '_xzd_ai_processed_generate_tags', json_encode([
                'status' => 'completed', 'mode' => 'merged', 'timestamp' => current_time('mysql'),
            ]));

            $results['generate_tags'] = [
                'tool_id' => 'generate_tags', 'success' => true,
                'data' => ['tags' => array_map(fn($n) => ['name' => $n, 'confidence' => 100], $allNames), 'count' => count($allNames)],
                'message' => "生成 " . count($allNames) . " 个标签" . (!empty($libraryTags) ? '（含词库' . count($libraryTags) . '个）' : ''),
            ];
        }

        // 3. SEO
        $seoStage = $this->findStage($stages, 'generate_seo');
        if ($seoStage) {
            $st = $data['seo_title'] ?? ''; $sk = $data['seo_keywords'] ?? ''; $sd = $data['seo_description'] ?? '';
            if ($st || $sk || $sd) {
                update_post_meta($postId, '_xzd_ai_seo_title', $st);
                update_post_meta($postId, '_xzd_ai_seo_keywords', $sk);
                update_post_meta($postId, '_xzd_ai_seo_description', $sd);
                update_post_meta($postId, '_xzd_ai_seo_data', ['seo_title' => $st, 'seo_keywords' => $sk, 'seo_description' => $sd]);
                update_post_meta($postId, '_xzd_ai_seo_content_hash', md5(wp_strip_all_tags(strip_shortcodes($post->post_content))));
                update_post_meta($postId, '_xzd_ai_processed_generate_seo', json_encode([
                    'status' => 'completed', 'mode' => 'merged', 'timestamp' => current_time('mysql'),
                ]));
                $results['generate_seo'] = [
                    'tool_id' => 'generate_seo', 'success' => true,
                    'data' => ['seo_title' => $st, 'seo_keywords' => $sk, 'seo_description' => $sd],
                    'message' => 'SEO完成',
                ];
            }
        }

        // 4. 摘要
        if ($this->hasStage($stages, 'generate_summary') && isset($data['summary']) && is_string($data['summary'])) {
            update_post_meta($postId, '_xzd_ai_summary', $data['summary']);
            update_post_meta($postId, '_xzd_ai_processed_generate_summary', json_encode([
                'status' => 'completed', 'mode' => 'merged', 'timestamp' => current_time('mysql'),
            ]));
            $results['generate_summary'] = [
                'tool_id' => 'generate_summary', 'success' => true,
                'data' => ['summary' => $data['summary']], 'message' => '摘要完成',
            ];
        }

        // 5. 内链（无润色独立模式）
        $autolinkStage = $this->findStage($stages, 'build_autolink');
        if ($autolinkStage && !$hasPolish && isset($data['autolinks']) && is_array($data['autolinks'])) {
            $colors = [
                'link_color'      => $autolinkStage['params']['link_color'] ?? '#2F6BFF',
                'link_weight'     => $autolinkStage['params']['link_weight'] ?? '500',
                'text_decoration' => $autolinkStage['params']['text_decoration'] ?? 'underline',
            ];
            $inserted = 0;
            $cur = $post->post_content;

            foreach ($data['autolinks'] as $link) {
                $anchor = $link['anchor_text'] ?? ''; $keyword = $link['keyword'] ?? '';
                if (empty($anchor) || empty($keyword)) continue;

                $targetPosts = get_posts([
                    's' => $keyword, 'post_type' => 'post', 'post_status' => 'publish',
                    'posts_per_page' => 1, 'exclude' => [$postId], 'orderby' => 'relevance',
                ]);
                if (empty($targetPosts)) continue;

                $url   = get_permalink($targetPosts[0]->ID);
                $title = esc_attr($targetPosts[0]->post_title);
                $html  = "<a href=\"{$url}\" title=\"{$title}\" class=\"xzd-autolink\" style=\"color:{$colors['link_color']} !important;font-weight:{$colors['link_weight']} !important;text-decoration:{$colors['text_decoration']} !important;border-bottom:1px dashed {$colors['link_color']}\">{$anchor}</a>";

                $pos = mb_stripos($cur, $anchor);
                if ($pos === false) continue;
                $before = mb_substr($cur, max(0, $pos - 100), 100);
                $after  = mb_substr($cur, $pos, mb_strlen($anchor) + 10);
                if (preg_match('/<a\b[^>]*>' . preg_quote($anchor, '/') . '/ui', $before . $after)) continue;

                $cur = substr_replace($cur, $html, $pos, mb_strlen($anchor));
                $inserted++;
            }

            if ($inserted > 0) wp_update_post(['ID' => $postId, 'post_content' => $cur]);
            update_post_meta($postId, '_xzd_ai_autolinks', current_time('mysql'));
            update_post_meta($postId, '_xzd_ai_processed_build_autolink', json_encode([
                'status' => 'completed', 'mode' => 'merged', 'timestamp' => current_time('mysql'),
            ]));
            $results['build_autolink'] = [
                'tool_id' => 'build_autolink', 'success' => true,
                'data' => ['inserted' => $inserted], 'message' => "内链 {$inserted} 个",
            ];
        }

        return $results;
    }

    // 内链标记替换

    /**
     * 替换 <a data-xzd="搜索词">锚文本</a> 为真实链接
     */
    private function replaceLinkMarkers(string $content, int $postId, array $colors, int &$count): string
    {
        $count = 0;

        $content = preg_replace_callback(
            self::LINK_PATTERN,
            function ($m) use ($postId, $colors, &$count) {
                $keyword = trim($m[1]);
                $anchor  = trim($m[2]);
                if (empty($keyword) || empty($anchor)) return $anchor;

                $targetPosts = get_posts([
                    's' => $keyword, 'post_type' => 'post', 'post_status' => 'publish',
                    'posts_per_page' => 1, 'exclude' => [$postId], 'orderby' => 'relevance',
                ]);

                if (empty($targetPosts)) {
                    Logger::info('merged_pipeline', "内链未匹配: kw={$keyword}, anchor={$anchor}");
                    return $anchor;
                }

                $url   = get_permalink($targetPosts[0]->ID);
                $title = esc_attr($targetPosts[0]->post_title);
                $count++;

                return sprintf(
                    '<a href="%s" title="%s" class="xzd-autolink" style="color:%s !important;font-weight:%s !important;text-decoration:%s !important;border-bottom:1px dashed %s">%s</a>',
                    esc_url($url), $title,
                    $colors['link_color'], $colors['link_weight'], $colors['text_decoration'], $colors['link_color'],
                    $anchor
                );
            },
            $content
        );

        return $content;
    }

    // 兜底清理

    private function stripRelatedReading(string $content): string
    {
        $content = preg_replace('/\n*(?:<p>)?\s*(?:相关阅读|推荐阅读|延伸阅读|扩展阅读)(?:：|:)\s*.*?(?:<\/p>)?\s*\n*/u', "\n", $content);
        $content = preg_replace('/\n*<!--\s*xzd-autolink-unmatched\s*-->[\s\S]*?<!--\s*\/xzd-autolink-unmatched\s*-->\s*\n*/u', "\n", $content);
        $content = preg_replace('/<p>\s*<\/p>\s*\n*/u', '', $content);
        return trim($content);
    }

    // HTML保护/恢复

    private function protectBlocks(string $html): array
    {
        $blocks = []; $i = 0;

        // 注意：不保护 <a data-xzd="..."> 标签（它们是需要被处理的标记，不是要保留的链接）

        // img
        $html = preg_replace_callback('/<img\b[^>]*\/?>/i', function ($m) use (&$blocks, &$i) {
            $k = "%%IMG_{$i}%%"; $blocks[$k] = $m[0]; $i++; return $k;
        }, $html);

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

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

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

        // pre>code
        $html = preg_replace_callback('/<pre\b[^>]*><code\b[^>]*>.*?<\/code><\/pre>/s', function ($m) use (&$blocks, &$i) {
            $k = "%%CODE_{$i}%%"; $blocks[$k] = $m[0]; $i++; return $k;
        }, $html);

        // Gutenberg非文本块
        $html = preg_replace_callback('/<!--\s*wp:([a-z]+\/[a-z]+)\b.*?\/wp:\1\s*-->/s', function ($m) use (&$blocks, &$i) {
            $skip = ['core/paragraph','core/heading','core/list','core/list-item','core/quote','core/pullquote'];
            if (in_array($m[1], $skip)) return $m[0];
            $k = "%%GB_{$i}%%"; $blocks[$k] = $m[0]; $i++; return $k;
        }, $html);

        // 已有链接（排除 data-xzd 标记链接）
        $html = preg_replace_callback('/<a\b(?!.*data-xzd=)[^>]*>.*?<\/a>/is', function ($m) use (&$blocks, &$i) {
            $k = "%%LINK_{$i}%%"; $blocks[$k] = $m[0]; $i++; return $k;
        }, $html);

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

    private function restoreBlocks(string $html, array $blocks): string
    {
        $keys = array_keys($blocks);
        usort($keys, fn($a, $b) => strlen($b) - strlen($a));
        foreach ($keys as $key) $html = str_replace($key, $blocks[$key], $html);
        return $html;
    }

    // 辅助方法

    private function shouldSkipByProcessingMode(int $postId, string $stageId, array $params): bool
    {
        if (($params['processing_mode'] ?? 'fill_empty') !== 'fill_empty') return false;
        $m = get_post_meta($postId, '_xzd_ai_processed_' . $stageId, true);
        $d = $m ? json_decode($m, true) : null;
        return !empty($d) && ($d['status'] ?? '') === 'completed';
    }

    private function pickModel(array $stages): string
    {
        $ps = $this->findStage($stages, 'polish_content');
        if ($ps && !empty($ps['params']['model'])) return $ps['params']['model'];
        foreach ($stages as $s) { if (!empty($s['params']['model'])) return $s['params']['model']; }
        return '';
    }

    private function hasStage(array $stages, string $id): bool { return $this->findStage($stages, $id) !== null; }

    private function findStage(array $stages, string $id): ?array
    {
        foreach ($stages as $s) { if (($s['id'] ?? '') === $id) return $s; }
        return null;
    }

    private function executeIndependent(int $postId, array $stage): array
    {
        $tid = $stage['id'] ?? '';
        $params = array_merge(
            ['post_id' => $postId, '_processing_mode' => $stage['params']['processing_mode'] ?? 'fill_empty'],
            $stage['params'] ?? []
        );
        try {
            $prev = ToolRegistry::enterSystemContext();
            try { $r = ToolRegistry::instance()->execute($tid, $params, 0); }
            finally { ToolRegistry::restoreSystemContext($prev); }
            return ['tool_id' => $tid, 'success' => $r->success, 'data' => $r->data, 'message' => $r->message];
        } catch (\Throwable $e) {
            Logger::error('merged_pipeline_independent', "{$tid} 失败: " . $e->getMessage());
            return ['tool_id' => $tid, 'success' => false, 'message' => $e->getMessage()];
        }
    }

    private function scheduleImageStage(int $postId, array $stage): array
    {
        $tid = $stage['id'] ?? '';
        $dk = "_xzd_async_scheduled_{$tid}";
        if (get_post_meta($postId, $dk, true) && (time() - (int)get_post_meta($postId, $dk, true)) < 300) {
            return ['tool_id' => $tid, 'executed' => false, 'message' => '5分钟内已调度'];
        }
        wp_schedule_single_event(time() + 5, 'xzd_ai_pipeline_async_stage', [[
            'post_id' => $postId, 'tool_id' => $tid, 'params' => $stage['params'] ?? [],
        ]]);
        update_post_meta($postId, $dk, time());
        return ['tool_id' => $tid, 'executed' => true, 'message' => '已调度异步执行'];
    }

    private function fallbackIndividual(int $postId, array $stages): array
    {
        Logger::warning('merged_pipeline', '融合失败降级: post=' . $postId);
        return array_map(fn($s) => $this->executeIndependent($postId, $s), $stages);
    }

    // ──────────────────────────────────────
    // 标签优化辅助方法（自包含，不依赖加密文件）
    // ──────────────────────────────────────

    /**
     * 获取站点热门标签池
     */
    private static function getTopTags(int $limit = 100): array
    {
        global $wpdb;
        return $wpdb->get_results($wpdb->prepare(
            "SELECT t.term_id, t.name, tt.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 tt.count DESC
             LIMIT %d", $limit
        ), ARRAY_A) ?: [];
    }

    /**
     * 语义相似度检查 — 共同字符比例判定
     */
    private static function findSimilarTag(string $newName, int $threshold = 80): ?array
    {
        $newLen = mb_strlen($newName);
        if ($newLen < 2) return null;

        $newChars = preg_split('//u', $newName, -1, PREG_SPLIT_NO_EMPTY) ?: [];
        $topTags = self::getTopTags(50);

        $bestMatch = null;
        $bestScore = 0;

        foreach ($topTags as $tag) {
            $tagName = $tag['name'];
            if ($newName === $tagName) return $tag;
            $tagLen = mb_strlen($tagName);
            $lenDiff = abs($newLen - $tagLen) / max($newLen, $tagLen);
            if ($lenDiff > 0.3) continue;

            $tagChars = preg_split('//u', $tagName, -1, PREG_SPLIT_NO_EMPTY) ?: [];
            $common = count(array_intersect($newChars, $tagChars));
            if ($common < 2) continue;

            $score = ($common * 2) / ($newLen + $tagLen) * 100;
            if ($score > $bestScore && $score >= $threshold) {
                $bestScore = $score;
                $bestMatch = $tag;
            }
        }
        return $bestMatch;
    }
}
