<?php
/**
 * 二次风控（byApiRisk）处理，供 f_check.php 与模拟测试复用
 */
require_once dirname(__DIR__) . '/DbHelper.php';
require_once __DIR__ . '/FpUrlHelper.php';
require_once __DIR__ . '/VisitorApiAudit.php';
require_once __DIR__ . '/RiskSecondaryGate.php';
require_once __DIR__ . '/CloakJudgmentCache.php';
require_once __DIR__ . '/RiskSecondaryCache.php';
require_once __DIR__ . '/VisitorVisitContext.php';
require_once __DIR__ . '/ReasonHelper.php';
require_once __DIR__ . '/VisitorRepository.php';
require_once __DIR__ . '/VisitorLogSchema.php';
require_once __DIR__ . '/ClientIpResolver.php';

class FCheckHandler
{
    /**
     * @param string $hdata base64(JSON)
     * @return array{ok:bool,status?:int,result?:bool,reason?:string,link?:string,message?:string,fingerprint_info?:array,api_request?:array,api_response?:array,cached?:bool}
     */
    public static function processFromHdata($hdata)
    {
        $decodedString = base64_decode((string) $hdata, true);
        $fingerprint_info = is_string($decodedString) ? json_decode($decodedString, true) : null;
        if (!is_array($fingerprint_info) || empty($fingerprint_info['domain'])) {
            return [
                'ok'      => false,
                'status'  => 400,
                'message' => '无效的指纹数据',
            ];
        }
        return self::process($fingerprint_info, (string) $hdata);
    }

    /**
     * @param array       $fingerprint_info
     * @param string|null $hdata base64 payload sent to API；为空时自动生成
     * @return array{ok:bool,status?:int,result?:bool,reason?:string,link?:string,message?:string,fingerprint_info?:array,api_request?:array,api_response?:array,cached?:bool}
     */
    public static function process(array $fingerprint_info, $hdata = null)
    {
        $fp_host = parse_url($fingerprint_info['domain'], PHP_URL_HOST);
        if (empty($fp_host)) {
            $fp_host = $_SERVER['HTTP_HOST'] ?? '';
        }
        if (!class_exists('ConfigLoader', false)) {
            require_once dirname(__DIR__, 2) . '/config/ConfigLoader.php';
        }
        $explicitConfig = trim((string) ($fingerprint_info['config_name'] ?? ''));
        $config_name = ConfigLoader::loadForFingerprint(
            (string) ($fingerprint_info['domain'] ?? ''),
            $fp_host,
            $explicitConfig !== '' ? $explicitConfig : null
        );
        $log_id      = (int) ($fingerprint_info['log_id'] ?? 0);
        if ($log_id <= 0) {
            $log_id = VisitorVisitContext::currentLogId();
        }

        if (!RiskSecondaryGate::isEnabled()) {
            $_SESSION['visit_to_3']   = 3;
            $_SESSION['check_result'] = 'true';

            $resolved = FpUrlHelper::resolve($config_name, true);
            $fp_url   = $resolved['url'];
            $response = [
                'reason' => '',
                'result' => true,
                'link'   => $fp_url !== '' ? $fp_url : FpUrlHelper::fallbackUrl(),
            ];

            if ($log_id > 0) {
                self::updateLog($log_id, [
                    'result' => 'true',
                    'reason' => '',
                    'fp_url' => $response['link'],
                ], $fingerprint_info, null, null, false);
            }

            return array_merge(['ok' => true, 'status' => 200, 'fingerprint_info' => $fingerprint_info], $response);
        }

        $clientIp = ClientIpResolver::resolve();
        $cached   = RiskSecondaryCache::get($clientIp);
        if ($cached !== null) {
            return self::finishFromCache($cached, $config_name, $log_id, $fingerprint_info);
        }

        if ($hdata === null || $hdata === '') {
            $json = json_encode($fingerprint_info, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            $hdata = base64_encode($json !== false ? $json : '{}');
        }

        $jsonData = [
            'id'            => COSTM_IP_SCORE,
            'hdata'         => $hdata,
            'referer'       => $fingerprint_info['referer'] ?? '',
            'domain'        => $fingerprint_info['domain'],
            'ip'            => $fingerprint_info['ip'] ?? '',
            'risk_enhanced' => defined('CLOAK_RISK_ENHANCED') && strtoupper(CLOAK_RISK_ENHANCED) === 'ON' ? 1 : 0,
            'risk_number'   => CLOAK_RISK_NUMBER,
        ];

        $ch = curl_init('www.tiktokba.com/cloak/byApiRisk');
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
        curl_setopt($ch, CURLOPT_USERAGENT, get_SERVER_value('HTTP_USER_AGENT'));
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
        curl_setopt($ch, CURLOPT_ENCODING, '');
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Content-type: application/x-www-form-urlencoded',
            'escloak-key: ' . CHECK_KEY,
        ]);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($jsonData));
        if (function_exists('forward_response_cookies')) {
            curl_setopt($ch, CURLOPT_HEADERFUNCTION, 'forward_response_cookies');
        }
        if (!empty($_COOKIE) && function_exists('encode_visitor_cookies')) {
            curl_setopt($ch, CURLOPT_COOKIE, encode_visitor_cookies());
        }

        $returnRaw = curl_exec($ch);
        if ($returnRaw === false) {
            $err = curl_error($ch);
            curl_close($ch);
            return [
                'ok'      => false,
                'status'  => 502,
                'message' => 'Curl error: ' . $err,
            ];
        }
        curl_close($ch);

        $return = json_decode($returnRaw, true);
        if (!is_array($return)) {
            return [
                'ok'      => false,
                'status'  => 502,
                'message' => '风控 API 返回无效',
            ];
        }

        return self::finishFromApi($return, $config_name, $log_id, $fingerprint_info, $jsonData, $clientIp);
    }

    /**
     * @param array $return byApiRisk 原始响应
     * @param array $jsonData 请求体
     */
    private static function finishFromApi(array $return, $config_name, $log_id, array $fingerprint_info, array $jsonData, $clientIp)
    {
        $_SESSION['visit_to_3'] = 3;
        $response               = [];
        $response['reason']     = $return['reason'] ?? '';
        $response['result']     = !empty($return['result']);

        if ($response['result']) {
            $_SESSION['check_result'] = 'true';
            $resolved                 = FpUrlHelper::resolve($config_name, true);
            $response['link']         = $resolved['url'];
        } else {
            $_SESSION['check_result'] = 'false';
            if (CLOAK_REDIRECT_METHOD == 'curl') {
                $response['link'] = '';
            } else {
                $response['link'] = DB_ZP;
            }
        }

        $update_log           = self::buildUpdateLog($response);
        self::persistCache($update_log, $clientIp);

        if ($log_id > 0) {
            self::updateLog($log_id, $update_log, $fingerprint_info, $jsonData, $return, true);
        }

        return array_merge([
            'ok'               => true,
            'status'           => 200,
            'fingerprint_info' => $fingerprint_info,
            'api_request'      => $jsonData,
            'api_response'     => $return,
            'cached'           => false,
        ], $response);
    }

    /**
     * @param array $cached RiskSecondaryCache::get()
     */
    private static function finishFromCache(array $cached, $config_name, $log_id, array $fingerprint_info)
    {
        RiskSecondaryCache::applyToSession($cached);
        $response = RiskSecondaryCache::toApiResponse($cached);

        $resultStr = $response['result'] ? 'true' : 'false';
        $update_log = [
            'result' => $resultStr,
            'reason' => cloak_cache_hit_log_reason($resultStr),
            'fp_url' => trim((string) ($response['link'] ?? '')),
        ];
        if ($update_log['fp_url'] === '') {
            $update_log['fp_url'] = $update_log['result'] === 'true'
                ? FpUrlHelper::fallbackUrl()
                : (defined('DB_ZP') ? (string) DB_ZP : '');
        }

        if ($log_id > 0) {
            self::updateLog($log_id, $update_log, $fingerprint_info, null, null, false);
        }

        return array_merge([
            'ok'               => true,
            'status'           => 200,
            'fingerprint_info' => $fingerprint_info,
            'cached'           => true,
        ], $response);
    }

    /**
     * @param array{result:bool,reason?:string,link?:string} $response
     * @return array{result:string,reason:string,fp_url:string}
     */
    private static function buildUpdateLog(array $response)
    {
        $update_log           = [];
        $update_log['result'] = !empty($response['result']) ? 'true' : 'false';
        $update_log['reason'] = cloak_normalize_log_reason(
            $update_log['result'],
            $response['reason'] ?? ''
        );
        $update_log['fp_url'] = trim((string) ($response['link'] ?? ''));
        if ($update_log['fp_url'] === '') {
            $update_log['fp_url'] = $update_log['result'] === 'true'
                ? FpUrlHelper::fallbackUrl()
                : (defined('DB_ZP') ? (string) DB_ZP : '');
        }

        return $update_log;
    }

    /**
     * @param array{result:string,reason:string,fp_url:string} $update_log
     * @param string                                           $clientIp
     */
    private static function persistCache(array $update_log, $clientIp)
    {
        CloakJudgmentCache::storeFromByApiRisk([
            'ip'      => $clientIp,
            'country' => (string) ($_SESSION['gcu_country'] ?? ''),
            'result'  => $update_log['result'],
            'reason'  => $update_log['reason'],
            'fp_url'  => $update_log['fp_url'],
        ]);
    }

    /**
     * @param int         $log_id
     * @param array       $update_log result, reason, fp_url
     * @param array       $fingerprint_info
     * @param array|null  $apiRequest
     * @param array|null  $apiResponse
     * @param bool        $includeFpAndApi 缓存路径不写 fp_hdata / api_risk_*
     */
    public static function updateLog($log_id, array $update_log, array $fingerprint_info, $apiRequest, $apiResponse, $includeFpAndApi = true)
    {
        $payload = [
            'result' => (string) ($update_log['result'] ?? ''),
            'reason' => cloak_normalize_log_reason(
                $update_log['result'] ?? '',
                $update_log['reason'] ?? ''
            ),
            'fp_url' => (string) ($update_log['fp_url'] ?? ''),
        ];

        if ($includeFpAndApi) {
            $fpHdataJson = json_encode($fingerprint_info, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            if ($fpHdataJson === false) {
                $fpHdataJson = '';
            }
            $payload['fp_hdata']            = $fpHdataJson;
            $payload['api_risk_request']    = $apiRequest !== null ? VisitorApiAudit::encode($apiRequest) : '';
            $payload['api_risk_response']   = $apiResponse !== null ? VisitorApiAudit::encode($apiResponse) : '';
        }

        $hadForceSync = !empty($GLOBALS['__cloak_force_sync_log']);
        $GLOBALS['__cloak_force_sync_log'] = true;
        $ok = cloak_update_log_db((int) $log_id, $payload, ['skip_enrich' => true]) !== false;
        if (!$hadForceSync) {
            unset($GLOBALS['__cloak_force_sync_log']);
        }

        return $ok;
    }
}
