<?php

namespace Kylin\WordFilter\Services;

use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
use Kylin\WordFilter\Enums\FilterMode;
use Kylin\WordFilter\Enums\Severity;
use Kylin\WordFilter\Enums\WordStatus;
use Kylin\WordFilter\Models\SensitiveWord;

/**
 * 敏感词过滤核心服务
 *
 * 基于 Trie 树 + DFA（确定有限自动机）算法实现高效敏感词匹配，
 * 支持全角/半角统一处理、批量过滤、多种过滤模式。
 *
 * 合规要求：
 *  - 依据《网络安全法》第四十七条，内容审核操作需记录审计日志
 *  - 依据等保 2.0 (GB/T 22239) 8.1.4.6，敏感词检测行为需可追溯
 */
class WordFilterService
{
    /**
     * Trie 树根节点
     *
     * @var array<string, mixed>
     */
    private array $trie = [];

    /**
     * 是否已初始化
     */
    private bool $initialized = false;

    public function __construct(
        private readonly VariantDetector $variantDetector,
    ) {}

    /**
     * 过滤文本
     *
     * @param  string  $text  待过滤文本
     * @param  array<string, mixed>|null  $scope  过滤范围（可限定分类、严重程度等）
     * @return array{original: string, filtered: string, mode: string, matches: array<int, array{word: string, position: int, length: int, severity: string, category: string}>, has_sensitive: bool, risk_level: string}
     */
    public function filter(string $text, ?array $scope = null): array
    {
        $this->ensureInitialized();

        $normalizedText = $this->variantDetector->normalizeText($text);
        $mode = FilterMode::from(config('kylin-word-filter.filter_mode', 'replace'));
        $replaceChar = config('kylin-word-filter.replace_char', '*');

        $matches = $this->searchTrie($normalizedText, $scope);

        $filtered = $text;
        $maxSeverity = Severity::Low;
        $matchResults = [];

        // 按位置倒序排列，避免替换时偏移
        usort($matches, fn (array $a, array $b) => $b['position'] <=> $a['position']);

        foreach ($matches as $match) {
            $severity = Severity::from($match['severity']);
            if ($severity->weight() > $maxSeverity->weight()) {
                $maxSeverity = $severity;
            }

            $matchResults[] = [
                'word' => $match['word'],
                'position' => $match['position'],
                'length' => $match['length'],
                'severity' => $match['severity'],
                'category' => $match['category'],
            ];

            if ($mode === FilterMode::Replace) {
                $filtered = mb_substr($filtered, 0, $match['position'])
                    . str_repeat($replaceChar, $match['length'])
                    . mb_substr($filtered, $match['position'] + $match['length']);
            }
        }

        // 按位置正序排列返回结果
        usort($matchResults, fn (array $a, array $b) => $a['position'] <=> $b['position']);

        $result = [
            'original' => $text,
            'filtered' => $filtered,
            'mode' => $mode->value,
            'matches' => $matchResults,
            'has_sensitive' => ! empty($matchResults),
            'risk_level' => $maxSeverity->value,
        ];

        // 审计日志：记录敏感词检测行为（仅当检测到敏感词时）
        if (! empty($matchResults)) {
            $this->auditFilter($text, $result);
        }

        return $result;
    }

    /**
     * 批量过滤
     *
     * @param  array<int, string>  $texts  待过滤文本数组
     * @param  array<string, mixed>|null  $scope  过滤范围
     * @return array<int, array{original: string, filtered: string, mode: string, matches: array, has_sensitive: bool, risk_level: string}>
     */
    public function batchFilter(array $texts, ?array $scope = null): array
    {
        $maxSize = config('kylin-word-filter.batch_max_size', 100);
        if (count($texts) > $maxSize) {
            return [];
        }

        return array_map(fn (string $text) => $this->filter($text, $scope), $texts);
    }

    /**
     * 检测是否包含敏感词
     */
    public function hasSensitiveWord(string $text): bool
    {
        $this->ensureInitialized();

        $normalizedText = $this->variantDetector->normalizeText($text);
        $matches = $this->searchTrie($normalizedText);

        return ! empty($matches);
    }

    /**
     * 重建 Trie 树
     */
    public function rebuildTrie(): void
    {
        $this->trie = [];
        $this->initialized = false;

        Cache::forget(config('kylin-word-filter.trie_cache_key'));
        $this->ensureInitialized();
    }

    /**
     * 获取 Trie 树（调试用）
     *
     * @return array<string, mixed>
     */
    public function getTrie(): array
    {
        $this->ensureInitialized();
        return $this->trie;
    }

    /**
     * 确保引擎已初始化
     */
    private function ensureInitialized(): void
    {
        if ($this->initialized) {
            return;
        }

        $cacheKey = config('kylin-word-filter.trie_cache_key');
        $cacheTtl = config('kylin-word-filter.cache_ttl', 3600);

        $cached = Cache::get($cacheKey);
        if ($cached !== null) {
            $this->trie = $cached;
            $this->initialized = true;
            return;
        }

        $this->buildTrie();

        Cache::put($cacheKey, $this->trie, $cacheTtl);
        $this->initialized = true;
    }

    /**
     * 从数据库构建 Trie 树
     */
    private function buildTrie(): void
    {
        $this->trie = [];

        $words = SensitiveWord::query()
            ->active()
            ->with('category')
            ->get();

        foreach ($words as $word) {
            $normalizedWord = $this->variantDetector->normalizeText($word->word);
            $this->insertWord(
                $normalizedWord,
                $word->severity->value,
                $word->category?->code ?? 'unknown',
            );
        }
    }

    /**
     * 向 Trie 树插入一个词
     */
    private function insertWord(string $word, string $severity, string $category): void
    {
        $node = &$this->trie;
        $length = mb_strlen($word);

        for ($i = 0; $i < $length; $i++) {
            $char = mb_substr($word, $i, 1);

            if (! isset($node[$char])) {
                $node[$char] = [];
            }

            $node = &$node[$char];
        }

        // 叶子节点标记：is_end + 元数据
        $node['is_end'] = true;
        $node['word'] = $word;
        $node['severity'] = $severity;
        $node['category'] = $category;
        $node['length'] = $length;
    }

    /**
     * 在 Trie 树中搜索匹配
     *
     * @param  string  $text  待搜索文本
     * @param  array<string, mixed>|null  $scope  过滤范围
     * @return array<int, array{word: string, position: int, length: int, severity: string, category: string}>
     */
    private function searchTrie(string $text, ?array $scope = null): array
    {
        $matches = [];
        $textLength = mb_strlen($text);

        for ($i = 0; $i < $textLength; $i++) {
            $node = $this->trie;
            $j = $i;

            while ($j < $textLength) {
                $char = mb_substr($text, $j, 1);

                if (! isset($node[$char])) {
                    break;
                }

                $node = $node[$char];

                if (isset($node['is_end']) && $node['is_end'] === true) {
                    $match = [
                        'word' => $node['word'],
                        'position' => $i,
                        'length' => $node['length'],
                        'severity' => $node['severity'],
                        'category' => $node['category'],
                    ];

                    if ($this->matchScope($match, $scope)) {
                        $matches[] = $match;
                    }
                }

                $j++;
            }
        }

        return $this->deduplicateMatches($matches);
    }

    /**
     * 检查匹配结果是否符合过滤范围
     */
    private function matchScope(array $match, ?array $scope): bool
    {
        if ($scope === null) {
            return true;
        }

        // 按分类筛选
        if (isset($scope['categories']) && is_array($scope['categories'])) {
            if (! in_array($match['category'], $scope['categories'], true)) {
                return false;
            }
        }

        // 按严重程度筛选
        if (isset($scope['severities']) && is_array($scope['severities'])) {
            if (! in_array($match['severity'], $scope['severities'], true)) {
                return false;
            }
        }

        // 按最低严重程度筛选
        if (isset($scope['min_severity'])) {
            $minSeverity = Severity::from($scope['min_severity']);
            $matchSeverity = Severity::from($match['severity']);
            if ($matchSeverity->weight() < $minSeverity->weight()) {
                return false;
            }
        }

        return true;
    }

    /**
     * 去重匹配结果（同一位置取最长匹配）
     *
     * @param  array<int, array{word: string, position: int, length: int, severity: string, category: string}>  $matches
     * @return array<int, array{word: string, position: int, length: int, severity: string, category: string}>
     */
    private function deduplicateMatches(array $matches): array
    {
        $deduplicated = [];

        foreach ($matches as $match) {
            $key = $match['position'];

            if (! isset($deduplicated[$key])) {
                $deduplicated[$key] = $match;
            } elseif ($match['length'] > $deduplicated[$key]['length']) {
                // 同位置取最长匹配
                $deduplicated[$key] = $match;
            }
        }

        return array_values($deduplicated);
    }

    /**
     * 记录敏感词检测审计日志
     *
     * 依据《网络安全法》第四十七条和等保 2.0 (GB/T 22239) 8.1.4.6，
     * 敏感词检测行为需记录审计日志，便于内容审核追溯。
     *
     * 审计日志写入失败不中断业务，仅记录 warning 日志。
     * 原文不记录到审计日志（避免敏感词二次传播），仅记录命中词和风险等级。
     */
    private function auditFilter(string $text, array $result): void
    {
        if (! class_exists(\Kylin\AuditLog\Models\AuditLog::class)) {
            return;
        }

        try {
            \Kylin\AuditLog\Models\AuditLog::create([
                'event' => 'sensitive_word_filtered',
                'model_type' => 'Kylin\WordFilter\Services\WordFilterService',
                'model_id' => 0,
                'new_values' => [
                    'mode' => $result['mode'],
                    'risk_level' => $result['risk_level'],
                    'match_count' => count($result['matches']),
                    'matched_words' => array_map(fn ($m) => [
                        'word' => $m['word'],
                        'category' => $m['category'],
                        'severity' => $m['severity'],
                    ], $result['matches']),
                    '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' => 'word-filter',
            ]);
        } catch (\Throwable $e) {
            logger()->warning('敏感词检测审计日志写入失败', [
                'error' => $e->getMessage(),
                'match_count' => count($result['matches']),
            ]);
        }
    }
}
