<?php

declare(strict_types=1);

namespace Kylin\RiskEngine\Services;

use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Support\Facades\Log;
use Kylin\RiskEngine\Enums\RiskDecision;
use Kylin\RiskEngine\Enums\RiskEventType;
use Kylin\RiskEngine\Models\RiskEvent;

/**
 * 风控核心服务 — 评分卡 + 决策路由 + 事件记录
 *
 * 设计要点：
 * - 纯函数计算，便于单元测试；
 * - 配置驱动规则，支持热更；
 * - 评分采用累加方式，超过阈值即触发对应动作；
 * - 事件记录支持审计追踪。
 */
final class RiskEngineService
{
    public function __construct(
        private readonly array $rules,
        private readonly Cache $cache,
    ) {
    }

    /**
     * 评估用户当前风险等级。
     *
     * @param  int    $userId
     * @param  int    $tenantId
     * @param  array  $signals  输入信号 ['device_account_switch_count' => 4, 'ip_registrations_24h' => 6, ...]
     * @param  array  $context  附加上下文 ['device_id' => 'xxx', 'ip' => '1.2.3.4', 'fingerprint' => 'xxx', 'geo' => 'Beijing']
     * @return array{score:int, decision:RiskDecision, hits:array<string>}
     */
    public function evaluate(int $userId, int $tenantId, array $signals, array $context = []): array
    {
        $score = 0;
        $hits = [];

        // 黑白名单优先检查（绕过评分卡）
        $ip = $context['ip'] ?? null;
        $deviceId = $context['device_id'] ?? null;
        $fingerprint = $context['fingerprint'] ?? null;

        $blacklistDecision = $this->checkBlackWhiteList($userId, $ip, $deviceId, $fingerprint);
        if ($blacklistDecision !== null) {
            return $blacklistDecision;
        }

        foreach ($this->rules as $key => $rule) {
            $value = $signals[$key . '_count'] ?? null;
            if ($value === null) {
                continue;
            }
            if (isset($rule['threshold']) && $value >= $rule['threshold']) {
                $score += (int) $rule['score'];
                $hits[] = $key;
            } elseif (isset($rule['ratio']) && $value >= $rule['ratio']) {
                $score += (int) $rule['score'];
                $hits[] = $key;
            }
        }

        $thresholds = config('kylin-risk-engine.thresholds');
        $decision = match (true) {
            $score <= ($thresholds['pass'] ?? 30)          => RiskDecision::PASS,
            $score <= ($thresholds['observe'] ?? 60)       => RiskDecision::OBSERVE,
            $score <= ($thresholds['face_required'] ?? 100) => RiskDecision::FACE_REQUIRED,
            default                                         => RiskDecision::LOCKOUT,
        };

        // 缓存评分
        $this->cache->put(
            sprintf('risk:score:%d', $userId),
            $score,
            now()->addDay()
        );

        // 记录风险事件（仅非 PASS 时）
        if ($decision !== RiskDecision::PASS && ! empty($hits)) {
            $this->recordEvents($userId, $tenantId, $hits, $score, $context);
            $this->audit('risk_decision_triggered', $userId, [
                'tenant_id' => $tenantId,
                'score' => $score,
                'decision' => $decision->value,
                'hits' => $hits,
                'ip' => $context['ip'] ?? null,
                'device_id' => $context['device_id'] ?? null,
            ]);
        }

        return ['score' => $score, 'decision' => $decision, 'hits' => $hits];
    }

    /**
     * 获取已缓存的风险评分（24h 内有效）。
     */
    public function getCachedScore(int $userId): ?int
    {
        return $this->cache->get(sprintf('risk:score:%d', $userId));
    }

    /**
     * 获取缓存的决策结果。
     */
    public function getCachedDecision(int $userId): ?RiskDecision
    {
        $score = $this->getCachedScore($userId);
        if ($score === null) {
            return null;
        }

        $thresholds = config('kylin-risk-engine.thresholds');
        return match (true) {
            $score <= ($thresholds['pass'] ?? 30)          => RiskDecision::PASS,
            $score <= ($thresholds['observe'] ?? 60)       => RiskDecision::OBSERVE,
            $score <= ($thresholds['face_required'] ?? 100) => RiskDecision::FACE_REQUIRED,
            default                                         => RiskDecision::LOCKOUT,
        };
    }

    /**
     * 检查黑白名单（优先级高于评分卡）
     *
     * 依据《反洗钱法》《个人信息保护法》：
     * - 黑名单用户直接拒绝
     * - 白名单用户直接放行
     *
     * @return array{score:int, decision:RiskDecision, hits:array<string>}|null  返回 null 表示未命中名单，继续评分卡流程
     */
    private function checkBlackWhiteList(int $userId, ?string $ip, ?string $deviceId, ?string $fingerprint): ?array
    {
        // 白名单优先：任一维度命中即放行
        $whitelistChecks = [
            ['user', (string) $userId],
        ];
        if ($ip !== null) {
            $whitelistChecks[] = ['ip', $ip];
        }
        if ($deviceId !== null) {
            $whitelistChecks[] = ['device', $deviceId];
        }
        if ($fingerprint !== null) {
            $whitelistChecks[] = ['fingerprint', $fingerprint];
        }

        foreach ($whitelistChecks as [$type, $value]) {
            if (BlackWhiteList::isWhitelisted($type, $value)) {
                return [
                    'score'    => 0,
                    'decision' => RiskDecision::PASS,
                    'hits'     => ['whitelist'],
                ];
            }
        }

        // 黑名单检查：任一维度命中即拒绝
        $blacklistChecks = [
            ['user', (string) $userId],
        ];
        if ($ip !== null) {
            $blacklistChecks[] = ['ip', $ip];
        }
        if ($deviceId !== null) {
            $blacklistChecks[] = ['device', $deviceId];
        }
        if ($fingerprint !== null) {
            $blacklistChecks[] = ['fingerprint', $fingerprint];
        }

        foreach ($blacklistChecks as [$type, $value]) {
            if (BlackWhiteList::isBlacklisted($type, $value)) {
                return [
                    'score'    => 999,
                    'decision' => RiskDecision::LOCKOUT,
                    'hits'     => ['blacklist'],
                ];
            }
        }

        return null;
    }

    /**
     * 记录风险事件到数据库。
     *
     * @param  array<string> $hits
     */
    private function recordEvents(int $userId, int $tenantId, array $hits, int $totalScore, array $context): void
    {
        foreach ($hits as $hit) {
            $eventType = RiskEventType::tryFrom($hit);
            $ruleScore = (int) ($this->rules[$hit]['score'] ?? 0);

            RiskEvent::create([
                'tenant_id'   => $tenantId,
                'user_id'     => $userId,
                'type'        => $eventType?->value ?? $hit,
                'score'       => $ruleScore,
                'device_id'   => $context['device_id'] ?? null,
                'fingerprint' => $context['fingerprint'] ?? null,
                'ip'          => $context['ip'] ?? null,
                'geo'         => $context['geo'] ?? null,
                'payload'     => array_merge($context, ['total_score' => $totalScore, 'all_hits' => $hits]),
                'handled'     => false,
            ]);
        }
    }

    /**
     * 记录风控审计日志
     *
     * 依据《反洗钱法》《网络安全法》和等保 2.0 (GB/T 22239)，
     * 风控决策触发需记录审计日志，便于风控合规审计和追溯。
     */
    private function audit(string $event, int $userId, array $extra = []): void
    {
        if (! class_exists(\Kylin\AuditLog\Models\AuditLog::class)) {
            return;
        }

        try {
            \Kylin\AuditLog\Models\AuditLog::create([
                'event' => $event,
                'model_type' => 'user',
                'model_id' => $userId,
                'new_values' => $extra,
                'old_values' => null,
                'user_id' => auth()->id(),
                'ip' => request()?->ip(),
                'url' => request()?->fullUrl(),
                'tag' => 'risk-engine',
            ]);
        } catch (\Throwable $e) {
            Log::warning('风控审计日志写入失败', [
                'error' => $e->getMessage(),
                'user_id' => $userId,
                'event' => $event,
            ]);
        }
    }
}
