<?php

declare(strict_types=1);

namespace Kylin\SensitiveWord\Services;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Kylin\SensitiveWord\Models\SensitiveWord;
use Kylin\SensitiveWord\Models\WordCategory;
use RuntimeException;

/**
 * 敏感词过滤服务
 *
 * 依据法规：
 * - 《网络安全法》第十二条、第四十七条
 * - 《网络信息内容生态治理规定》第六条、第九条
 * - TC260-003 内容安全标准
 *
 * 功能：
 * - DFA 算法高效匹配
 * - 17 类风险分类
 * - 抗绕过检测：符号插入、谐音替换、拆字、全角/繁体/零宽字符
 * - 三种过滤模式：replace/block/warn
 * - 词库版本管理与增量同步
 * - 批量导入
 * - 审计日志
 */
class SensitiveWordService
{
    private ?array $trieTree = null;

    /**
     * 检测文本是否包含敏感词
     */
    public function contains(string $text): bool
    {
        return !empty($this->detect($text));
    }

    /**
     * 检测文本中的敏感词
     *
     * @return array{word: string, category: string, risk_level: string, position: int}[]
     */
    public function detect(string $text): array
    {
        $tree = $this->getTrieTree();
        $results = [];

        // 文本标准化
        $normalized = $this->normalizeText($text);

        // 原文检测
        $this->searchInTree($normalized, $tree, $results);

        // 抗绕过：去除符号后检测
        if (config('kylin-sensitive-word.variant_detection.symbol_insert', true)) {
            $cleaned = $this->removeSymbols($normalized);
            if ($cleaned !== $normalized) {
                $this->searchInTree($cleaned, $tree, $results);
            }
        }

        // 抗绕过：谐音还原后检测
        if (config('kylin-sensitive-word.variant_detection.homophone', true)) {
            $restored = $this->restoreHomophones($normalized);
            if ($restored !== $normalized) {
                $this->searchInTree($restored, $tree, $results);
            }
        }

        // 抗绕过：拆字合并后检测
        if (config('kylin-sensitive-word.variant_detection.char_split', true)) {
            $merged = $this->mergeSplitChars($normalized);
            if ($merged !== $normalized) {
                $this->searchInTree($merged, $tree, $results);
            }
        }

        // 去重
        $unique = [];
        foreach ($results as $r) {
            $key = $r['word'];
            if (!isset($unique[$key])) {
                $unique[$key] = $r;
            }
        }

        // 审计日志
        if (!empty($unique) && config('kylin-sensitive-word.audit_enabled', true)) {
            $this->auditDetection($text, array_values($unique));
        }

        return array_values($unique);
    }

    /**
     * 过滤文本，根据配置模式处理敏感词
     *
     * 模式说明：
     * - replace: 替换敏感词为 * 号
     * - block:   检测到敏感词抛出异常，拦截发布
     * - warn:    检测到敏感词仅记录警告，不修改文本
     *
     * @throws RuntimeException 当模式为 block 且检测到高风险敏感词时
     */
    public function filter(string $text, ?string $mode = null): string
    {
        $mode = $mode ?? config('kylin-sensitive-word.filter_mode', 'replace');
        $hits = $this->detect($text);

        if (empty($hits)) {
            return $text;
        }

        // block 模式：高风险敏感词直接拦截
        if ($mode === 'block') {
            $highRisk = array_filter($hits, fn ($h) => $h['risk_level'] === 'high');
            if (!empty($highRisk)) {
                $words = implode(', ', array_column($highRisk, 'word'));
                throw new RuntimeException("检测到高风险敏感词，内容已被拦截: {$words}");
            }
        }

        // warn 模式：仅记录警告，不修改文本
        if ($mode === 'warn') {
            Log::warning('敏感词检测警告', [
                'hits' => array_column($hits, 'word'),
                'text_preview' => mb_substr($text, 0, 100),
            ]);
            return $text;
        }

        // replace 模式：替换敏感词
        $replaceChar = config('kylin-sensitive-word.replace_char', '*');
        foreach ($hits as $hit) {
            $replacement = str_repeat($replaceChar, mb_strlen($hit['word']));
            $text = str_replace($hit['word'], $replacement, $text);
        }

        return $text;
    }

    /**
     * 添加敏感词
     */
    public function addWord(string $word, string $category, string $riskLevel = 'medium', ?string $source = 'manual'): SensitiveWord
    {
        $model = SensitiveWord::firstOrCreate(
            ['word' => $word, 'category' => $category],
            [
                'risk_level' => $riskLevel,
                'status' => 1,
                'version' => $this->getCurrentVersion(),
                'source' => $source,
            ]
        );

        $this->clearCache();
        return $model;
    }

    /**
     * 批量导入敏感词
     *
     * @param array $words [['word' => '词', 'category' => '分类', 'risk_level' => '等级'], ...]
     * @return array{imported: int, skipped: int}
     */
    public function batchImport(array $words, string $source = 'import'): array
    {
        $max = config('kylin-sensitive-word.batch_import_max', 10000);
        $words = array_slice($words, 0, $max);

        $version = $this->getCurrentVersion();
        $imported = 0;
        $skipped = 0;

        foreach ($words as $item) {
            $exists = SensitiveWord::where('word', $item['word'])
                ->where('category', $item['category'] ?? 'insult')
                ->exists();

            if ($exists) {
                $skipped++;
                continue;
            }

            SensitiveWord::create([
                'word' => $item['word'],
                'category' => $item['category'] ?? 'insult',
                'risk_level' => $item['risk_level'] ?? 'medium',
                'status' => 1,
                'version' => $version,
                'source' => $source,
            ]);
            $imported++;
        }

        $this->clearCache();

        Log::info('敏感词批量导入完成', [
            'imported' => $imported,
            'skipped' => $skipped,
            'source' => $source,
        ]);

        return ['imported' => $imported, 'skipped' => $skipped];
    }

    /**
     * 增量同步：获取指定版本之后的新增/更新词
     *
     * @return array{words: array, version: string}
     */
    public function sync(string $sinceVersion = '0.0.0'): array
    {
        $syncUrl = config('kylin-sensitive-word.sync_url');

        // 本地增量查询（无远程 URL 时从本地获取）
        if (empty($syncUrl)) {
            $words = SensitiveWord::where('version', '>', $sinceVersion)
                ->where('status', 1)
                ->get(['word', 'category', 'risk_level', 'version'])
                ->toArray();

            return [
                'words' => $words,
                'version' => $this->getCurrentVersion(),
            ];
        }

        // 远程同步
        $timeout = config('kylin-sensitive-word.sync_timeout', 30);
        $response = \Http::timeout($timeout)->get($syncUrl, ['since' => $sinceVersion]);

        if (!$response->successful()) {
            throw new RuntimeException("词库同步失败: HTTP {$response->status()}");
        }

        $data = $response->json();
        $imported = 0;

        if (!empty($data['words'])) {
            $result = $this->batchImport($data['words'], 'sync');
            $imported = $result['imported'];
        }

        Log::info('敏感词远程同步完成', [
            'since' => $sinceVersion,
            'imported' => $imported,
            'version' => $data['version'] ?? 'unknown',
        ]);

        return [
            'words' => $data['words'] ?? [],
            'version' => $data['version'] ?? $this->getCurrentVersion(),
        ];
    }

    /**
     * 获取当前词库版本
     */
    public function getCurrentVersion(): string
    {
        return SensitiveWord::max('version') ?? '1.0.0';
    }

    /**
     * 获取分类列表
     *
     * @return array
     */
    public function getCategories(): array
    {
        return Cache::remember(
            config('kylin-sensitive-word.cache_prefix') . 'categories',
            config('kylin-sensitive-word.cache_ttl', 3600),
            fn () => WordCategory::active()->orderBy('sort')->get()->toArray()
        );
    }

    /**
     * 初始化默认分类
     */
    public function initDefaultCategories(): void
    {
        $categories = config('kylin-sensitive-word.categories', []);

        foreach ($categories as $code => $info) {
            WordCategory::firstOrCreate(
                ['code' => $code],
                [
                    'name' => $info['name'] ?? $code,
                    'risk_level' => $info['level'] ?? 'medium',
                    'status' => true,
                    'sort' => 0,
                ]
            );
        }
    }

    /**
     * 获取词库统计
     *
     * @return array{total: int, by_category: array, by_risk_level: array}
     */
    public function getStatistics(): array
    {
        $total = SensitiveWord::where('status', 1)->count();

        $byCategory = SensitiveWord::where('status', 1)
            ->select('category', DB::raw('count(*) as count'))
            ->groupBy('category')
            ->pluck('count', 'category')
            ->toArray();

        $byRiskLevel = SensitiveWord::where('status', 1)
            ->select('risk_level', DB::raw('count(*) as count'))
            ->groupBy('risk_level')
            ->pluck('count', 'risk_level')
            ->toArray();

        return [
            'total' => $total,
            'by_category' => $byCategory,
            'by_risk_level' => $byRiskLevel,
        ];
    }

    /**
     * DFA 树搜索
     */
    private function searchInTree(string $text, array $tree, array &$results): void
    {
        $len = mb_strlen($text);
        for ($i = 0; $i < $len; $i++) {
            $char = mb_substr($text, $i, 1);
            if (!isset($tree[$char])) {
                continue;
            }

            $node = $tree[$char];
            $word = $char;
            $j = $i + 1;

            while (true) {
                if (isset($node['is_end']) && $node['is_end'] === true) {
                    $results[] = [
                        'word' => $word,
                        'category' => $node['category'] ?? 'unknown',
                        'risk_level' => $node['risk_level'] ?? 'medium',
                        'position' => $i,
                    ];
                }
                if ($j >= $len) {
                    break;
                }
                $nextChar = mb_substr($text, $j, 1);
                if (!isset($node['children'][$nextChar])) {
                    break;
                }
                $word .= $nextChar;
                $node = $node['children'][$nextChar];
                $j++;
            }
        }
    }

    /**
     * 构建 DFA Trie 树（带缓存）
     */
    private function getTrieTree(): array
    {
        if ($this->trieTree !== null) {
            return $this->trieTree;
        }

        $cacheKey = config('kylin-sensitive-word.cache_prefix') . 'trie';

        return $this->trieTree = Cache::remember($cacheKey, config('kylin-sensitive-word.cache_ttl', 3600), function () {
            $tree = [];
            $words = DB::table('ks_sensitive_words')
                ->where('status', 1)
                ->select(['word', 'category', 'risk_level'])
                ->get();

            foreach ($words as $row) {
                $this->insertWord($tree, $row->word, $row->category, $row->risk_level);
            }

            return $tree;
        });
    }

    /**
     * 向 Trie 树插入词
     */
    private function insertWord(array &$tree, string $word, string $category, string $riskLevel): void
    {
        $chars = mb_str_split($word);
        $node = &$tree;

        foreach ($chars as $char) {
            if (!isset($node[$char])) {
                $node[$char] = ['children' => []];
            }
            $node = &$node[$char]['children'];
        }

        $node['is_end'] = true;
        $node['category'] = $category;
        $node['risk_level'] = $riskLevel;
    }

    /**
     * 文本标准化
     * - 全角转半角
     * - 繁体转简体（如安装了简繁转换包）
     * - 去除零宽字符
     */
    private function normalizeText(string $text): string
    {
        // 全角转半角
        if (config('kylin-sensitive-word.variant_detection.full_to_half', true)) {
            $text = $this->fullToHalf($text);
        }

        // 去除零宽字符
        if (config('kylin-sensitive-word.variant_detection.zero_width', true)) {
            $text = preg_replace('/[\x{200B}-\x{200D}\x{FEFF}]/u', '', $text);
        }

        return $text;
    }

    /**
     * 全角转半角
     */
    private function fullToHalf(string $text): string
    {
        $map = [];
        for ($i = 0xFF01; $i <= 0xFF5E; $i++) {
            $map[mb_chr($i, 'UTF-8')] = mb_chr($i - 0xFEE0, 'UTF-8');
        }
        $map[mb_chr(0x3000, 'UTF-8')] = ' ';

        return strtr($text, $map);
    }

    /**
     * 去除常见干扰符号
     */
    private function removeSymbols(string $text): string
    {
        return preg_replace('/[\s\-_\.\*\!\@\#\$\%\^\&\(\)\+\=\/\\\|~`<>?,;:\'"\[\]{}]/u', '', $text);
    }

    /**
     * 谐音还原映射
     */
    private function restoreHomophones(string $text): string
    {
        $map = [
            '0' => '零', '1' => '一', '2' => '二', '3' => '三', '4' => '四',
            '5' => '五', '6' => '六', '7' => '七', '8' => '八', '9' => '九',
            '吧' => '八', '发' => '法', '去' => '法',
        ];

        return str_replace(array_keys($map), array_values($map), $text);
    }

    /**
     * 拆字合并：将常见的拆字还原
     * 如 "日月" -> "明"，"木几" -> "机"
     */
    private function mergeSplitChars(string $text): string
    {
        $map = [
            '日月' => '明', '木几' => '机', '女子' => '好', '口十' => '叶',
            '门木' => '闲', '门日' => '间', '门口' => '问', '门心' => '闷',
            '水青' => '清', '水白' => '泉', '水月' => '清', '火丁' => '灯',
            '土也' => '地', '土里' => '埋', '人言' => '信', '人门' => '闪',
            '弓长' => '张', '木公' => '松', '木白' => '柏', '木子' => '李',
        ];

        return strtr($text, $map);
    }

    /**
     * 审计日志：记录敏感词检测行为
     *
     * 依据《网络安全法》第四十七条，网络运营者应记录用户发布信息的管理日志；
     * 依据等保 2.0 (GB/T 22239) 8.1.4.6，敏感词检测行为需可追溯。
     * 同时写入 Log channel 和审计日志表（如可用）。
     * 原文不记录到审计日志表（避免敏感词二次传播），仅记录命中词和风险等级。
     */
    private function auditDetection(string $text, array $hits): void
    {
        $hitSummary = array_map(fn ($h) => [
            'word' => $h['word'],
            'category' => $h['category'],
            'risk_level' => $h['risk_level'],
        ], $hits);

        Log::channel(config('kylin-sensitive-word.audit_log_channel', 'stack'))
            ->info('敏感词检测审计', [
                'hits' => $hitSummary,
                'text_preview' => mb_substr($text, 0, 200),
                'hit_count' => count($hits),
                'timestamp' => now()->toIso8601String(),
            ]);

        if (! class_exists(\Kylin\AuditLog\Models\AuditLog::class)) {
            return;
        }

        try {
            \Kylin\AuditLog\Models\AuditLog::create([
                'event' => 'sensitive_word_detected',
                'model_type' => 'Kylin\SensitiveWord\Services\SensitiveWordService',
                'model_id' => 0,
                'new_values' => [
                    'hits' => $hitSummary,
                    'hit_count' => count($hits),
                    'text_length' => mb_strlen($text),
                    'text_preview' => mb_substr($text, 0, 50),
                ],
                'old_values' => null,
                'user_id' => auth()->id(),
                'ip' => request()?->ip(),
                'url' => request()?->fullUrl(),
                'tag' => 'sensitive-word',
            ]);
        } catch (\Throwable $e) {
            Log::warning('敏感词检测审计日志写入审计表失败', [
                'error' => $e->getMessage(),
                'hit_count' => count($hits),
            ]);
        }
    }

    /**
     * 清除 Trie 缓存
     */
    public function clearCache(): void
    {
        Cache::forget(config('kylin-sensitive-word.cache_prefix') . 'trie');
        Cache::forget(config('kylin-sensitive-word.cache_prefix') . 'categories');
        $this->trieTree = null;
    }
}
