<?php

namespace XZD\AI\V5\SEO;

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

/**
 * TagGovernance — 标签自动治理
 *
 * 功能：
 *   1. 健康度巡检（每周自动扫描全部标签）
 *   2. 碎片/死亡标签自动清理
 *   3. 五层合并建议生成
 *   4. 相似标签检测
 *
 * @since V3.2.0
 */
class TagGovernance
{
    /** @var string 巡检冷却 transient key */
    const SCAN_COOLDOWN_KEY = 'xzd_tag_governance_last_scan';

    /** @var int 巡检间隔（秒） */
    const SCAN_INTERVAL = 604800; // 7天

    /** @var int 合并阈值：字符重叠 */
    const CHAR_OVERLAP_THRESHOLD = 65;

    /** @var int 碎片标签：1篇文章且超过N天 */
    const FRAGMENT_DAYS = 90;

    /** @var int 虚弱标签：1-2篇文章且超过N天 */
    const WEAK_DAYS = 30;

    /** @var array 泛词黑名单 */
    const GENERIC_WORDS = [
        '教程', '入门', '技巧', '方法', '经验', '总结', '分享',
        '笔记', '心得', '指南', '攻略', '推荐', '介绍', '详解',
        '资料', '资源', '工具', '软件', '最新', '热门', '干货',
    ];

    // ============================================================
    // 健康度巡检
    // ============================================================

    /**
     * 执行完整标签巡检
     *
     * @return array{total:int, healthy:int, weak:int, dead:int, fragment:int, cleaned:int}
     */
    public function runHealthCheck(): array
    {
        global $wpdb;

        $now = current_time('mysql');
        $allTags = $this->getAllTagsWithStats();

        $stats = ['total' => count($allTags), 'healthy' => 0, 'weak' => 0, 'dead' => 0, 'fragment' => 0, 'cleaned' => 0];

        foreach ($allTags as $tag) {
            $count = (int) $tag['post_count'];
            $lastUsed = $tag['last_used'] ?? null;
            $termId = (int) $tag['term_id'];

            // 跳过保护标签
            if ($this->isProtected($termId)) {
                $stats['healthy']++;
                continue;
            }

            // 健康度分级
            if ($count === 0) {
                // 死亡标签：立即删除
                wp_delete_term($termId, 'post_tag');
                $wpdb->delete($wpdb->prefix . 'xzd_ai_tag_health', ['term_id' => $termId]);
                $stats['dead']++;
                $stats['cleaned']++;
                Logger::info('tag_governance', "清理死亡标签: {$tag['name']} (term_id={$termId})");
                continue;
            }

            $daysSinceLastUse = $lastUsed ? $this->daysAgo($lastUsed) : 999;
            $daysSinceCreation = $this->getTagAge($termId);

            if ($count === 1 && $daysSinceCreation > self::FRAGMENT_DAYS) {
                $status = 'fragment';
                $stats['fragment']++;
            } elseif ($count <= 2 && $daysSinceCreation > self::WEAK_DAYS) {
                $status = 'weak';
                $stats['weak']++;
            } else {
                $status = 'healthy';
                $stats['healthy']++;
            }

            // 更新健康度快照
            $this->upsertHealth($termId, $tag['name'], $count, $lastUsed, $status);
        }

        // 生成合并建议
        $this->generateMergeSuggestions();

        // 更新巡检时间
        set_transient(self::SCAN_COOLDOWN_KEY, time(), self::SCAN_INTERVAL);

        Logger::info('tag_governance', "巡检完成: 总{$stats['total']} 健康{$stats['healthy']} 虚弱{$stats['weak']} 碎片{$stats['fragment']} 清理{$stats['cleaned']}");
        return $stats;
    }

    /**
     * 是否需要巡检
     */
    public function needsScan(): bool
    {
        $last = get_transient(self::SCAN_COOLDOWN_KEY);
        return !$last || (time() - (int) $last) > self::SCAN_INTERVAL;
    }

    // ============================================================
    // 合并建议生成
    // ============================================================

    /**
     * 生成五层合并建议
     *
     * @return int 生成的建议数
     */
    public function generateMergeSuggestions(): int
    {
        global $wpdb;

        // 清空旧的pending建议（已处理的不动）
        $wpdb->query("DELETE FROM {$wpdb->prefix}xzd_ai_tag_merge_suggestions WHERE status = 'pending'");

        $allTags = $this->getAllTagsWithStats(3000); // 覆盖全部标签（不受1000限制）
        $suggestions = [];
        $seen = [];

        foreach ($allTags as $i => $tagA) {
            $nameA = $tagA['name'];
            $idA = (int) $tagA['term_id'];

            foreach ($allTags as $j => $tagB) {
                if ($j <= $i) continue; // 避免重复和自比较
                $nameB = $tagB['name'];
                $idB = (int) $tagB['term_id'];

                $pairKey = min($idA, $idB) . '_' . max($idA, $idB);
                if (isset($seen[$pairKey])) continue;

                $result = $this->compareTags($nameA, $nameB, $tagA, $tagB);
                if ($result) {
                    $seen[$pairKey] = true;
                    $suggestions[] = $result;
                }
            }
        }

        // 去重后写入
        $inserted = 0;
        foreach ($suggestions as $s) {
            $wpdb->insert($wpdb->prefix . 'xzd_ai_tag_merge_suggestions', [
                'source_term_id' => $s['source_id'],
                'target_term_id' => $s['target_id'],
                'source_name'    => $s['source_name'],
                'target_name'    => $s['target_name'],
                'match_level'    => $s['level'],
                'score'          => $s['score'],
                'status'         => 'pending',
                'created_at'     => current_time('mysql'),
            ]);
            $inserted++;
        }

        return $inserted;
    }

    /**
     * 比较两个标签，返回合并建议或 null
     */
    private function compareTags(string $nameA, string $nameB, array $tagA, array $tagB): ?array
    {
        $idA = (int) $tagA['term_id'];
        $idB = (int) $tagB['term_id'];
        $countA = (int) $tagA['post_count'];
        $countB = (int) $tagB['post_count'];

        // 第一层：标准化后完全一致
        $normA = $this->normalize($nameA);
        $normB = $this->normalize($nameB);
        if ($normA === $normB) {
            return $this->suggest($idA, $idB, $nameA, $nameB, 'exact', 100, $countA, $countB);
        }

        // 第二层：子串包含
        $subResult = $this->checkSubstring($nameA, $nameB);
        if ($subResult) {
            return $this->suggest($idA, $idB, $nameA, $nameB, 'substring', $subResult['score'], $countA, $countB, $subResult['direction']);
        }

        // 第三层：共同字符比例
        $overlapScore = $this->charOverlapScore($nameA, $nameB);
        if ($overlapScore >= self::CHAR_OVERLAP_THRESHOLD) {
            return $this->suggest($idA, $idB, $nameA, $nameB, 'char_overlap', $overlapScore, $countA, $countB);
        }

        return null;

        // 第四层（AI语义）和第五层（使用模式）在后续迭代中实现
    }

    /**
     * 子串匹配检测
     */
    private function checkSubstring(string $a, string $b): ?array
    {
        $lenA = mb_strlen($a);
        $lenB = mb_strlen($b);

        // 短标签字符串必须 >= 2 才有意义
        $minLen = min($lenA, $lenB);
        if ($minLen < 2) return null;

        if (mb_stripos($a, $b) !== false) {
            // B 是 A 的子串 → 保留长标签 A
            $score = (int) round(($lenB / $lenA) * 100);
            return ['score' => $score, 'direction' => 'b_to_a'];
        }
        if (mb_stripos($b, $a) !== false) {
            // A 是 B 的子串 → 保留长标签 B
            $score = (int) round(($lenA / $lenB) * 100);
            return ['score' => $score, 'direction' => 'a_to_b'];
        }
        return null;
    }

    /**
     * 共同字符比例得分（改进版）
     *
     * 修复：旧算法对所有字符等权计算，导致 "Docker→DeepSeek" 等无关匹配。
     * 改进：
     *   1. 分离中文/非中文字符，中文字符权重更高
     *   2. 纯英文/数字标签至少需要3个共同字符
     *   3. 短标签（<3字符）要求精确匹配或子串匹配，不走字符重叠
     *   4. 过滤掉仅共享1-2个英文字母的无意义匹配
     */
    private function charOverlapScore(string $a, string $b): int
    {
        $charsA = preg_split('//u', $a, -1, PREG_SPLIT_NO_EMPTY) ?: [];
        $charsB = preg_split('//u', $b, -1, PREG_SPLIT_NO_EMPTY) ?: [];

        $lenA = count($charsA);
        $lenB = count($charsB);

        // 短标签保护：任一标签 < 3 字符时，要求子串匹配而非字符重叠
        if ($lenA < 3 || $lenB < 3) {
            return 0;
        }

        // 分离中文和非中文字符
        $isCJK = function(string $ch): bool {
            $cp = mb_ord($ch, 'UTF-8');
            return $cp !== null && (
                ($cp >= 0x4E00 && $cp <= 0x9FFF) ||  // CJK统一汉字
                ($cp >= 0x3400 && $cp <= 0x4DBF) ||  // CJK扩展A
                ($cp >= 0x20000 && $cp <= 0x2A6DF)   // CJK扩展B
            );
        };

        $cjkA = array_filter($charsA, $isCJK);
        $cjkB = array_filter($charsB, $isCJK);
        $nonCjkA = array_filter($charsA, fn($c) => !$isCJK($c));
        $nonCjkB = array_filter($charsB, fn($c) => !$isCJK($c));

        $commonCjk = count(array_intersect($cjkA, $cjkB));
        $commonNon = count(array_intersect($nonCjkA, $nonCjkB));

        // 纯英文/数字标签：至少需要3个共同字符才认为有意义
        $totalCjk = count($cjkA) + count($cjkB);
        if ($totalCjk === 0) {
            if ($commonNon < 3) return 0;
        }

        // 中文标签：至少需要1个共同汉字
        if ($totalCjk > 0 && $commonCjk === 0) {
            // 没有共同汉字 → 检查是否全靠非中文字符匹配
            // 如果两个标签都有中文但无共同汉字，大概率不相关
            if (count($cjkA) > 0 && count($cjkB) > 0) {
                return 0;
            }
        }

        // 加权计算：中文字符权重 2x
        $weightedCommon = $commonCjk * 2 + $commonNon;
        $weightedTotal = count($cjkA) * 2 + count($nonCjkA) + count($cjkB) * 2 + count($nonCjkB);

        if ($weightedTotal === 0) return 0;
        return (int) round(($weightedCommon * 2) / $weightedTotal * 100);
    }

    // ============================================================
    // 合并执行
    // ============================================================

    /**
     * 智能判断合并方向：返回建议的 [source, target]
     * 规则：①保护标签永远做target ②文章多的做target ③名字短的做target ④子串包含的做target
     */
    public static function suggestDirection(string $nameA, int $countA, string $nameB, int $countB, bool $aProtected = false, bool $bProtected = false): array
    {
        // 规则①：保护标签永远做target
        if ($aProtected && !$bProtected) return ['source' => $nameB, 'target' => $nameA];
        if ($bProtected && !$aProtected) return ['source' => $nameA, 'target' => $nameB];

        // 规则②：文章多的做target
        if ($countB > $countA) return ['source' => $nameA, 'target' => $nameB];
        if ($countA > $countB) return ['source' => $nameB, 'target' => $nameA];

        // 规则③：名字短的做target（更通用）
        $lenA = mb_strlen($nameA);
        $lenB = mb_strlen($nameB);
        if ($lenB < $lenA) return ['source' => $nameA, 'target' => $nameB];
        if ($lenA < $lenB) return ['source' => $nameB, 'target' => $nameA];

        // 规则④：子串包含
        if (mb_stripos($nameA, $nameB) !== false) return ['source' => $nameA, 'target' => $nameB];
        if (mb_stripos($nameB, $nameA) !== false) return ['source' => $nameB, 'target' => $nameA];

        // 默认保持原方向
        return ['source' => $nameA, 'target' => $nameB];
    }

    /**
     * 执行标签合并：将 source 的所有文章重新打 target 标签
     */
    public function mergeTags(int $sourceId, int $targetId): array
    {
        $sourceTerm = get_term($sourceId, 'post_tag');
        $targetTerm = get_term($targetId, 'post_tag');

        if (!$sourceTerm || is_wp_error($sourceTerm)) return ['success' => false, 'message' => '源标签不存在'];
        if (!$targetTerm || is_wp_error($targetTerm)) return ['success' => false, 'message' => '目标标签不存在'];

        // 🔄 智能方向修正
        $direction = self::suggestDirection(
            $sourceTerm->name, (int)$sourceTerm->count,
            $targetTerm->name, (int)$targetTerm->count,
            $this->isProtected($sourceId), $this->isProtected($targetId)
        );
        if ($direction['source'] !== $sourceTerm->name) {
            // 方向需要翻转
            $tmp = $sourceTerm;
            $sourceTerm = $targetTerm;
            $targetTerm = $tmp;
            $sourceId = $sourceTerm->term_id;
            $targetId = $targetTerm->term_id;
            Logger::info('tag_governance', "合并方向自动修正: {$direction['target']} → {$direction['source']} 反转为 {$sourceTerm->name} → {$targetTerm->name}");
        }

        global $wpdb;

        // 查找所有有源标签的文章
        $postIds = $wpdb->get_col($wpdb->prepare(
            "SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id = %d",
            $sourceTerm->term_taxonomy_id
        ));

        $count = 0;
        foreach ($postIds as $postId) {
            // 添加目标标签
            $result = wp_set_post_tags((int) $postId, $targetTerm->name, true);
            if (!is_wp_error($result)) $count++;
        }

        // 删除源标签
        wp_delete_term($sourceId, 'post_tag');

        // 更新合并建议表
        $wpdb->update(
            $wpdb->prefix . 'xzd_ai_tag_merge_suggestions',
            ['status' => 'merged', 'resolved_at' => current_time('mysql')],
            ['source_term_id' => $sourceId, 'target_term_id' => $targetId, 'status' => 'pending']
        );

        Logger::info('tag_governance', "合并标签: {$sourceTerm->name}→{$targetTerm->name}, 更新{$count}篇文章");

        // 写入同义词表，防止后续再次产生碎片标签
        self::addSynonym($sourceTerm->name, $targetTerm->name, 'manual');
        // 级联更新：如果 source 本身也是规范标签，其同义词全部迁移到 target
        self::cascadeCanonicalUpdate($sourceTerm->name, $targetTerm->name);

        return ['success' => true, 'message' => "已合并 {$count} 篇文章", 'count' => $count];
    }

    /**
     * 批量合并所有pending建议
     */
    public function mergeAllPending(): array
    {
        global $wpdb;
        $pending = $wpdb->get_results(
            "SELECT * FROM {$wpdb->prefix}xzd_ai_tag_merge_suggestions WHERE status = 'pending'",
            ARRAY_A
        );

        $merged = 0;
        foreach ($pending as $row) {
            $result = $this->mergeTags((int) $row['source_term_id'], (int) $row['target_term_id']);
            if ($result['success']) $merged++;
        }

        return ['success' => true, 'merged' => $merged, 'total' => count($pending)];
    }

    // ============================================================
    // 碎片清理
    // ============================================================

    /**
     * 清理碎片标签
     */
    public function cleanFragments(): array
    {
        global $wpdb;

        $fragments = $wpdb->get_results(
            "SELECT th.term_id, th.name, th.post_count
             FROM {$wpdb->prefix}xzd_ai_tag_health th
             WHERE th.health_status IN ('fragment', 'dead')
             AND th.protected = 0
             LIMIT 100",
            ARRAY_A
        );

        $cleaned = [];
        foreach ($fragments as $frag) {
            $termId = (int) $frag['term_id'];
            if ($this->isProtected($termId)) continue;

            wp_delete_term($termId, 'post_tag');
            $wpdb->delete($wpdb->prefix . 'xzd_ai_tag_health', ['term_id' => $termId]);
            $cleaned[] = $frag['name'];
        }

        return ['success' => true, 'cleaned' => count($cleaned), 'names' => $cleaned];
    }

    // ============================================================
    // 保护名单
    // ============================================================

    /**
     * 保护标签（标记为不自动清理/合并）
     */
    public function protectTag(int $termId): void
    {
        global $wpdb;
        $wpdb->update($wpdb->prefix . 'xzd_ai_tag_health', ['protected' => 1], ['term_id' => $termId]);
    }

    /**
     * 取消保护
     */
    public function unprotectTag(int $termId): void
    {
        global $wpdb;
        $wpdb->update($wpdb->prefix . 'xzd_ai_tag_health', ['protected' => 0], ['term_id' => $termId]);
    }

    /**
     * 是否被保护
     */
    public function isProtected(int $termId): bool
    {
        global $wpdb;
        return (bool) $wpdb->get_var($wpdb->prepare(
            "SELECT protected FROM {$wpdb->prefix}xzd_ai_tag_health WHERE term_id = %d",
            $termId
        ));
    }

    /**
     * 获取保护标签ID列表
     */
    public function getProtectedIds(): array
    {
        global $wpdb;
        return $wpdb->get_col(
            "SELECT term_id FROM {$wpdb->prefix}xzd_ai_tag_health WHERE protected = 1"
        );
    }

    // ============================================================
    // 查询接口
    // ============================================================

    /**
     * 获取健康度摘要（实时查询 WordPress 标签数据）
     */
    public function getHealthSummary(): array
    {
        global $wpdb;

        // 实时从 WordPress terms 表查询，确保数据最新
        $total = (int) $wpdb->get_var(
            "SELECT COUNT(*) FROM {$wpdb->terms} t
             INNER JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
             WHERE tt.taxonomy = 'post_tag'"
        );

        // 健康标签：有 2+ 篇文章
        $healthy = (int) $wpdb->get_var(
            "SELECT COUNT(*) FROM {$wpdb->term_taxonomy}
             WHERE taxonomy = 'post_tag' AND count >= 2"
        );

        // 虚弱标签：只有 1 篇文章
        $weak = (int) $wpdb->get_var(
            "SELECT COUNT(*) FROM {$wpdb->term_taxonomy}
             WHERE taxonomy = 'post_tag' AND count = 1"
        );

        // 碎片/死亡标签：0 篇文章
        $fragment = (int) $wpdb->get_var(
            "SELECT COUNT(*) FROM {$wpdb->term_taxonomy}
             WHERE taxonomy = 'post_tag' AND count = 0"
        );

        return [
            'total'    => $total,
            'healthy'  => $healthy,
            'weak'     => $weak,
            'fragment' => $fragment,
            'dead'     => $fragment, // 0文章的即碎片/死亡
        ];
    }

    /**
     * 获取合并建议列表
     */
    public function getMergeSuggestions(string $status = 'pending', int $limit = 50): array
    {
        global $wpdb;
        $table = $wpdb->prefix . 'xzd_ai_tag_merge_suggestions';
        $sql = "SELECT s.*,
                       COALESCE(st.count, 0) AS source_count,
                       COALESCE(tt.count, 0) AS target_count
                FROM {$table} s
                LEFT JOIN {$wpdb->prefix}term_taxonomy st
                  ON s.source_term_id = st.term_id AND st.taxonomy = 'post_tag'
                LEFT JOIN {$wpdb->prefix}term_taxonomy tt
                  ON s.target_term_id = tt.term_id AND tt.taxonomy = 'post_tag'
                WHERE s.status = %s
                ORDER BY s.score DESC";
        if ($limit > 0) {
            $sql .= $wpdb->prepare(" LIMIT %d", $limit);
        }
        return $wpdb->get_results($wpdb->prepare($sql, $status), ARRAY_A) ?: [];
    }

    /**
     * 忽略合并建议
     */
    public function ignoreSuggestion(int $id): void
    {
        global $wpdb;
        $wpdb->update(
            $wpdb->prefix . 'xzd_ai_tag_merge_suggestions',
            ['status' => 'ignored', 'resolved_at' => current_time('mysql')],
            ['id' => $id]
        );
    }

    // ============================================================
    // 私有辅助方法
    // ============================================================

    /**
     * 获取所有标签及其统计
     */
    private function getAllTagsWithStats(int $limit = 2000): array
    {
        global $wpdb;
        return $wpdb->get_results($wpdb->prepare(
            "SELECT t.term_id, t.name, tt.count AS post_count,
                    (SELECT MAX(p.post_date)
                     FROM {$wpdb->posts} p
                     JOIN {$wpdb->term_relationships} tr ON p.ID = tr.object_id
                     WHERE tr.term_taxonomy_id = tt.term_taxonomy_id
                     AND p.post_status = 'publish') AS last_used
             FROM {$wpdb->terms} t
             JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id
             WHERE tt.taxonomy = 'post_tag'
             ORDER BY tt.count DESC
             LIMIT %d",
            $limit
        ), ARRAY_A) ?: [];
    }

    /**
     * 标准化标签名（去除大小写、全半角差异）
     */
    private function normalize(string $name): string
    {
        // 全角转半角
        $name = mb_convert_kana($name, 'as', 'UTF-8');
        // 小写
        $name = mb_strtolower($name, 'UTF-8');
        // 去空格
        $name = trim($name);
        return $name;
    }

    /**
     * 插入或更新健康度记录
     */
    private function upsertHealth(int $termId, string $name, int $count, ?string $lastUsed, string $status): void
    {
        global $wpdb;
        $table = $wpdb->prefix . 'xzd_ai_tag_health';

        $exists = $wpdb->get_var($wpdb->prepare("SELECT term_id FROM {$table} WHERE term_id = %d", $termId));
        $data = [
            'name'          => $name,
            'post_count'    => $count,
            'last_used'     => $lastUsed,
            'health_status' => $status,
            'checked_at'    => current_time('mysql'),
        ];

        if ($exists) {
            $wpdb->update($table, $data, ['term_id' => $termId]);
        } else {
            $data['term_id'] = $termId;
            $wpdb->insert($table, $data);
        }
    }

    /**
     * 计算日期距今多少天
     */
    private function daysAgo(string $date): int
    {
        $ts = strtotime($date);
        if (!$ts) return 999;
        return (int) ((time() - $ts) / 86400);
    }

    /**
     * 获取标签年龄（天）
     */
    private function getTagAge(int $termId): int
    {
        $term = get_term($termId, 'post_tag');
        if (!$term || is_wp_error($term)) return 999;

        // WordPress 不直接存储 term 创建时间，通过最早关联的文章日期推断
        global $wpdb;
        $firstPostDate = $wpdb->get_var($wpdb->prepare(
            "SELECT MIN(p.post_date)
             FROM {$wpdb->posts} p
             JOIN {$wpdb->term_relationships} tr ON p.ID = tr.object_id
             WHERE tr.term_taxonomy_id = %d AND p.post_status = 'publish'",
            $term->term_taxonomy_id
        ));

        return $firstPostDate ? $this->daysAgo($firstPostDate) : 999;
    }

    /**
     * 生成合并建议条目
     */
    private function suggest(int $idA, int $idB, string $nameA, string $nameB, string $level, int $score, int $countA, int $countB, ?string $direction = null): array
    {
        // 文章多的作为目标（保留更热门的标签）
        if ($direction === 'a_to_b' || ($direction === null && $countB > $countA)) {
            return ['source_id' => $idA, 'target_id' => $idB, 'source_name' => $nameA, 'target_name' => $nameB, 'level' => $level, 'score' => $score, 'source_count' => $countA, 'target_count' => $countB];
        }
        return ['source_id' => $idB, 'target_id' => $idA, 'source_name' => $nameB, 'target_name' => $nameA, 'level' => $level, 'score' => $score, 'source_count' => $countB, 'target_count' => $countA];
    }

    // ============================================================
    // AI 一键合并
    // ============================================================

    /**
     * AI 分析标签库，返回合并建议
     */
    /**
     * AI 智能合并分析 — 将标签库发给AI，返回合并方案
     *
     * @param bool $useAI 是否使用AI（否则纯算法）
     * @return array{suggestions:array, ai_used:bool, total:int, target:int}
     */
    public function aiMergeAnalysis(bool $useAI = true): array
    {
        global $wpdb;

        // 获取所有标签（不受数量限制）
        $allTags = get_terms(['taxonomy' => 'post_tag', 'hide_empty' => false, 'number' => 0]);
        if (empty($allTags) || is_wp_error($allTags)) {
            return ['success' => false, 'message' => '标签库为空'];
        }

        $totalTags = count($allTags);
        if ($totalTags < 10) {
            return ['success' => false, 'message' => '标签太少，无需合并'];
        }

        // 目标标签数
        $tagCap = (int) \XZD\AI\V5\Core\Config::instance()->int('tag_library.max_total', 500);
        if ($tagCap <= 0) $tagCap = 500;
        $targetCount = min($tagCap, max(50, (int)($totalTags * 0.4)));

        // ==========================================
        // 阶段1：读取已有算法建议（秒级），避免每次重新扫描O(n²)
        // ==========================================
        $algoCandidates = $this->getMergeSuggestions('pending', 0); // 0=不限制，取全部
        if (empty($algoCandidates)) {
            // 首次使用或建议已清空时才重新扫描
            $this->generateMergeSuggestions();
            $algoCandidates = $this->getMergeSuggestions('pending', 0);
        }
        $protectedIds = $this->getProtectedIds();

        // 分离：高分(≥80)且非char_overlap的直接采纳，其余的送AI确认
        $autoAccept = [];   // 算法高分，直接采纳
        $aiCandidates = []; // 需要AI语义确认的
        $allSuggestions = [];

        foreach ($algoCandidates as $c) {
            $srcId = (int)($c['source_term_id'] ?? 0);
            $tgtId = (int)($c['target_term_id'] ?? 0);
            if (in_array($srcId, $protectedIds) || in_array($tgtId, $protectedIds)) continue;

            $srcCount = (int)($c['source_count'] ?? 0);
            $tgtCount = (int)($c['target_count'] ?? 0);

            $item = [
                'source'       => $c['source_name'] ?? '',
                'target'       => $c['target_name'] ?? '',
                'reason'       => '算法检测(' . ($c['match_level'] ?? '') . ', ' . ($c['score'] ?? 0) . '%)',
                'source_count' => $srcCount,
                'target_count' => $tgtCount,
                'match_level'  => $c['match_level'] ?? '',
                'score'        => (int)($c['score'] ?? 0),
                '_src_id'      => $srcId,
                '_tgt_id'      => $tgtId,
            ];

            if ($c['score'] >= 80 && ($c['match_level'] ?? '') !== 'char_overlap') {
                $autoAccept[] = $item;
                $allSuggestions[] = $item;
            } else {
                $aiCandidates[] = $item;
            }
        }

        // ==========================================
        // 阶段2：AI语义确认 — 只分析低置信度候选对（2-3批，20秒内）
        // ==========================================
        $aiConfirmed = [];
        if ($useAI && !empty($aiCandidates)) {
            $client = \XZD\AI\V5\AI\Client::instance();
            $batches = array_chunk($aiCandidates, 50); // 每批50对

            foreach ($batches as $bi => $batch) {
                // 构建简洁的候选对列表
                $pairs = [];
                foreach ($batch as $idx => $item) {
                    $pairs[] = ($idx + 1) . ". \"{$item['source']}\" → \"{$item['target']}\" (分数:{$item['score']})";
                }

                $prompt = "以下是算法检测到的标签合并候选对（第" . ($bi + 1) . "/" . count($batches) . "批）。\n"
                    . "请判断每对是否真的是同义词，应该合并：\n\n"
                    . implode("\n", $pairs) . "\n\n"
                    . "规则：1)只有确实同义或高度相关的才确认合并 2)含义不同的不要合并 3)source合并到target";

                try {
                    $result = $client->chatStructured(
                        [\XZD\AI\V5\AI\Types::system("你是标签管理专家。判断每对标签是否为同义词或高度相关。只确认确实应该合并的对，不确定的不要合并。"),
                         \XZD\AI\V5\AI\Types::user($prompt)],
                        ['type' => 'object', 'properties' => [
                            'confirmed' => ['type' => 'array', 'items' => ['type' => 'object', 'properties' => [
                                'pair_id' => ['type' => 'integer', 'description' => '候选对编号'],
                                'should_merge' => ['type' => 'boolean'],
                                'reason' => ['type' => 'string'],
                            ]]]],
                        ],
                        ['max_tokens' => 2000, 'disable_thinking' => true, 'timeout' => 20]
                    );

                    if (!empty($result['content']['confirmed'])) {
                        foreach ($result['content']['confirmed'] as $cf) {
                            $pid = (int)($cf['pair_id'] ?? 0) - 1; // 转回0-based索引
                            if (($cf['should_merge'] ?? false) && isset($batch[$pid])) {
                                $item = $batch[$pid];
                                $item['reason'] = 'AI确认: ' . ($cf['reason'] ?? '语义相同');
                                $item['ai_confirmed'] = true;
                                $aiConfirmed[] = $item;
                                $allSuggestions[] = $item;
                            }
                        }
                    }
                } catch (\Throwable $e) {
                    Logger::warning('tag_governance', 'AI语义确认失败(第' . ($bi + 1) . '批): ' . $e->getMessage());
                    // AI失败时，保留算法原始判断
                    foreach ($batch as $item) {
                        $aiConfirmed[] = $item;
                        $allSuggestions[] = $item;
                    }
                }
            }
        } elseif (!$useAI) {
            // 不使用AI时，算法候选全部纳入
            foreach ($aiCandidates as $item) {
                $allSuggestions[] = $item;
            }
        }

        // 去重
        $seen = [];
        $unique = [];
        foreach ($allSuggestions as $s) {
            $key = $s['source'] . '|' . $s['target'];
            if (isset($seen[$key])) continue;
            $seen[$key] = true;
            unset($s['_src_id'], $s['_tgt_id']); // 清理内部字段
            $unique[] = $s;
        }

        // 统计影响范围
        $totalImpact = 0;
        foreach ($unique as &$s) {
            $srcTerm = get_term_by('name', $s['source'], 'post_tag');
            $s['source_count'] = $srcTerm ? (int)$srcTerm->count : ($s['source_count'] ?? 0);
            $tgtTerm = get_term_by('name', $s['target'], 'post_tag');
            $s['target_count'] = $tgtTerm ? (int)$tgtTerm->count : ($s['target_count'] ?? 0);
            if ($srcTerm) {
                $s['impact_articles'] = (int)$wpdb->get_var($wpdb->prepare(
                    "SELECT COUNT(DISTINCT p.ID) FROM {$wpdb->posts} p
                     JOIN {$wpdb->term_relationships} tr ON p.ID = tr.object_id
                     WHERE tr.term_taxonomy_id = %d AND p.post_status = 'publish'",
                    $srcTerm->term_taxonomy_id
                ));
                $totalImpact += $s['impact_articles'];
            } else {
                $s['impact_articles'] = 0;
            }
        }
        unset($s);

        return [
            'success'        => true,
            'suggestions'    => array_values($unique),
            'ai_used'        => $useAI,
            'ai_confirmed'   => count($aiConfirmed),
            'auto_accepted'  => count($autoAccept),
            'total'          => $totalTags,
            'target'         => $targetCount,
            'total_impact'   => $totalImpact,
        ];
    }

    /**
     * 执行AI合并：删除源标签→替换为目标标签→补标签到配置数量
     */
    public function executeAiMerge(array $merges): array
    {
        global $wpdb;
        $merged = 0;
        $replaced = 0;
        $filled = 0;

        // 获取标签生成数量配置（从 pipeline.stages 数组中查找 generate_tags 阶段）
        $tagsPerPost = 3;
        $pipelineStages = \XZD\AI\V5\Core\Config::instance()->get('pipeline.stages', []);
        if (is_array($pipelineStages)) {
            foreach ($pipelineStages as $stage) {
                if (($stage['id'] ?? $stage['tool_id'] ?? '') === 'generate_tags') {
                    $tagsPerPost = (int) ($stage['params']['count'] ?? 3);
                    break;
                }
            }
        }
        if ($tagsPerPost < 1) $tagsPerPost = 3;

        foreach ($merges as $m) {
            $src = get_term_by('name', $m['source'] ?? '', 'post_tag');
            $tgt = get_term_by('name', $m['target'] ?? '', 'post_tag');
            if (!$src || !$tgt || $this->isProtected($src->term_id)) continue;

            // 查找使用源标签的文章
            $pids = $wpdb->get_col($wpdb->prepare(
                "SELECT p.ID FROM {$wpdb->posts} p
                 JOIN {$wpdb->term_relationships} tr ON p.ID = tr.object_id
                 WHERE tr.term_taxonomy_id = %d AND p.post_status = 'publish'",
                $src->term_taxonomy_id
            ));

            foreach ($pids as $pid) {
                $cur = wp_get_post_tags($pid, ['fields' => 'names']);
                $new = array_values(array_diff($cur, [$src->name]));
                if (!in_array($tgt->name, $new)) {
                    $new[] = $tgt->name;
                }

                // 补标签：确保每篇文章至少有 tagsPerPost 个标签
                if (count($new) < $tagsPerPost) {
                    $need = $tagsPerPost - count($new);
                    $existingNames = $new;
                    // 从词库热度取标签补充
                    $topTags = \XZD\AI\V5\Tools\ContentManager::getTopTags($need + 5);
                    foreach ($topTags as $tt) {
                        if ($need <= 0) break;
                        if (!in_array($tt['name'], $existingNames, true)) {
                            $new[] = $tt['name'];
                            $existingNames[] = $tt['name'];
                            $need--;
                            $filled++;
                        }
                    }
                }

                wp_set_post_tags($pid, $new, false);
                $replaced++;
            }

            wp_delete_term($src->term_id, 'post_tag');
            $merged++;
            
            // 写入同义词映射，防止后续再次产生碎片标签
            self::addSynonym($m['source'], $m['target'], 'ai_merge');
            // 级联：source 若是规范标签，同义词迁移到 target
            self::cascadeCanonicalUpdate($m['source'], $m['target']);
            
            Logger::info('tag_governance', "AI合并: {$src->name} → {$tgt->name}", ['posts' => count($pids)]);
        }

        return [
            'success'         => true,
            'merged'          => $merged,
            'replaced_posts'  => $replaced,
            'tags_filled'     => $filled,
            'tags_per_post'   => $tagsPerPost,
        ];
    }

    // ============================================================
    // 同义词映射系统
    // ============================================================

    /**
     * 添加同义词映射（变体名 → 规范名），自动分配 group_id + seq
     */
    public static function addSynonym(string $synonymName, string $canonicalName, string $source = 'ai_merge'): bool
    {
        global $wpdb;
        if (empty($synonymName) || empty($canonicalName) || $synonymName === $canonicalName) {
            return false;
        }
        $table = $wpdb->prefix . 'xzd_ai_tag_synonyms';

        // 检查是否已存在
        $exists = $wpdb->get_var($wpdb->prepare(
            "SELECT id FROM {$table} WHERE synonym_name = %s AND canonical_name = %s", $synonymName, $canonicalName
        ));
        if ($exists) return false;

        // 查找 canonical 所在的 group
        $groupId = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT group_id FROM {$table} WHERE synonym_name = %s AND canonical_name = %s AND seq = 0 LIMIT 1",
            $canonicalName, $canonicalName
        ));

        if (!$groupId) {
            // 规范标签不在表中 → 新建 group
            $groupId = (int) $wpdb->get_var("SELECT IFNULL(MAX(group_id), 0) + 1 FROM {$table}");
            // 先插入规范标签自身 (seq=0)
            $wpdb->insert($table, [
                'group_id' => $groupId, 'seq' => 0,
                'synonym_name' => $canonicalName, 'canonical_name' => $canonicalName,
                'source' => $source, 'created_at' => current_time('mysql'),
            ]);
        }

        // 该 group 内下一个 seq
        $nextSeq = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT IFNULL(MAX(seq), 0) + 1 FROM {$table} WHERE group_id = %d", $groupId
        ));

        $wpdb->insert($table, [
            'group_id' => $groupId, 'seq' => $nextSeq,
            'synonym_name' => $synonymName, 'canonical_name' => $canonicalName,
            'source' => $source, 'created_at' => current_time('mysql'),
        ]);
        return true;
    }

    /**
     * 级联更新：当规范标签被合并到另一个标签时，更新所有关联同义词
     */
    public static function cascadeCanonicalUpdate(string $oldCanonical, string $newCanonical): int
    {
        global $wpdb;
        $table = $wpdb->prefix . 'xzd_ai_tag_synonyms';

        // 找到 oldCanonical 的 group
        $oldGroup = $wpdb->get_var($wpdb->prepare(
            "SELECT group_id FROM {$table} WHERE synonym_name = %s AND canonical_name = %s AND seq = 0",
            $oldCanonical, $oldCanonical
        ));
        if (!$oldGroup) return 0;

        // 找到 newCanonical 的 group（可能不存在）
        $newGroup = $wpdb->get_var($wpdb->prepare(
            "SELECT group_id FROM {$table} WHERE synonym_name = %s AND canonical_name = %s AND seq = 0",
            $newCanonical, $newCanonical
        ));

        if ($newGroup && (int)$newGroup !== (int)$oldGroup) {
            // 两个 group 合并：把 oldGroup 的所有行迁到 newGroup
            $maxSeq = (int) $wpdb->get_var($wpdb->prepare(
                "SELECT IFNULL(MAX(seq), 0) FROM {$table} WHERE group_id = %d", $newGroup
            ));
            $rows = $wpdb->get_results($wpdb->prepare(
                "SELECT id, synonym_name, seq FROM {$table} WHERE group_id = %d ORDER BY seq", $oldGroup
            ), ARRAY_A);
            $moved = 0;
            foreach ($rows as $row) {
                if ($row['synonym_name'] === $oldCanonical && $row['seq'] == 0) {
                    // 旧规范标签变成新 group 的同义词
                    $maxSeq++;
                    $wpdb->update($table, [
                        'group_id' => $newGroup, 'seq' => $maxSeq,
                        'canonical_name' => $newCanonical,
                    ], ['id' => $row['id']]);
                } else {
                    $maxSeq++;
                    $wpdb->update($table, [
                        'group_id' => $newGroup, 'seq' => $maxSeq,
                        'canonical_name' => $newCanonical,
                    ], ['id' => $row['id']]);
                }
                $moved++;
            }
            return $moved;
        }

        if (!$newGroup) {
            // newCanonical 还没有 group → 直接改 old group 的规范名
            $wpdb->update($table, ['canonical_name' => $newCanonical], ['group_id' => $oldGroup]);
            // 把 seq=0 的行名也改了
            $wpdb->update($table, ['synonym_name' => $newCanonical], ['group_id' => $oldGroup, 'seq' => 0]);
            return (int) $wpdb->rows_affected;
        }

        return 0;
    }

    /**
     * 查询标签的规范名（如果存在同义词映射）
     * @return string|null 规范名，或 null 表示无需替换
     */
    public static function getCanonicalTag(string $tagName): ?string
    {
        global $wpdb;
        $table = $wpdb->prefix . 'xzd_ai_tag_synonyms';
        $canonical = $wpdb->get_var($wpdb->prepare(
            "SELECT canonical_name FROM {$table} WHERE synonym_name = %s LIMIT 1",
            $tagName
        ));
        return $canonical ?: null;
    }

    /**
     * 批量解析标签名 → 规范名
     * @param string[] $tagNames 原始标签名数组
     * @return string[] 替换后的标签名数组（去重）
     */
    public static function resolveSynonyms(array $tagNames): array
    {
        if (empty($tagNames)) return [];
        global $wpdb;
        $table = $wpdb->prefix . 'xzd_ai_tag_synonyms';
        
        // 构建 IN 查询
        $placeholders = implode(',', array_fill(0, count($tagNames), '%s'));
        $results = $wpdb->get_results($wpdb->prepare(
            "SELECT synonym_name, canonical_name FROM {$table} WHERE synonym_name IN ({$placeholders})",
            ...$tagNames
        ), ARRAY_A);
        
        // 构建映射表
        $map = [];
        foreach ($results as $row) {
            $map[$row['synonym_name']] = $row['canonical_name'];
        }
        
        // 替换
        $resolved = [];
        foreach ($tagNames as $name) {
            $resolved[] = $map[$name] ?? $name;
        }
        
        return array_values(array_unique($resolved));
    }

    /**
     * 获取同义词表统计
     */
    public static function getSynonymStats(): array
    {
        global $wpdb;
        $table = $wpdb->prefix . 'xzd_ai_tag_synonyms';
        $exists = $wpdb->get_var("SHOW TABLES LIKE '{$table}'") === $table;
        if (!$exists) return ['total_synonyms' => 0, 'canonical_count' => 0, 'table_size' => '0 KB'];

        $total = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
        $canonical = (int) $wpdb->get_var("SELECT COUNT(DISTINCT canonical_name) FROM {$table} WHERE canonical_name != ''");
        $groups = (int) $wpdb->get_var("SELECT COUNT(DISTINCT group_id) FROM {$table} WHERE group_id > 0");
        
        // 表大小
        $size = $wpdb->get_var("SELECT ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024, 1) 
            FROM information_schema.TABLES 
            WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '{$table}'");

        return [
            'total_synonyms'  => $total,
            'canonical_count' => $canonical,
            'group_count'     => $groups,
            'table_size'      => ($size ?: 0) . ' KB',
        ];
    }
}
