<?php
/**
 * 模拟测试访问：重建请求环境并跑完整判定链（含二次风控）
 */
require_once dirname(__DIR__, 2) . '/config/ConfigLoader.php';
require_once __DIR__ . '/SimulationRouteResolver.php';
require_once __DIR__ . '/FCheckHandler.php';
require_once __DIR__ . '/DomainRepository.php';

class VisitorSimulationRunner
{
    const REASON_PREFIX = '[模拟测试]';

    /**
     * @param array $input
     * @return array
     */
    public static function runManual(array $input)
    {
        $context = self::normalizeManualInput($input);
        if (!empty($context['error'])) {
            return ['ok' => false, 'message' => $context['error']];
        }
        return self::execute($context, null);
    }

    /**
     * @param int $logId
     * @return array
     */
    public static function replayFromLog($logId)
    {
        $row = self::fetchLogRow((int) $logId);
        if ($row === null) {
            return ['ok' => false, 'message' => '日志不存在'];
        }
        $context = self::contextFromLogRow($row);
        if (!empty($context['error'])) {
            return ['ok' => false, 'message' => $context['error']];
        }
        return self::execute($context, (int) $logId);
    }

    /**
     * @param int $logId
     * @return array
     */
    public static function prefillFromLog($logId)
    {
        $row = self::fetchLogRow((int) $logId);
        if ($row === null) {
            return ['ok' => false, 'message' => '日志不存在'];
        }
        $context = self::contextFromLogRow($row);
        if (!empty($context['error'])) {
            return ['ok' => false, 'message' => $context['error']];
        }
        return [
            'ok'      => true,
            'log_id'  => (int) $logId,
            'prefill' => [
                'config_name'       => $context['config_name'],
                'ip'                => $context['ip'],
                'domain'            => $context['domain'],
                'page_url'          => $context['page_url'],
                'referer'           => $context['referer'],
                'user_agent'        => $context['user_agent'],
                'accept_language'   => $context['accept_language'],
                'visit_md5_code'    => $context['visit_md5_code'],
                'cookies_json'      => $context['cookies_json'],
                'fp_hdata'          => $context['fp_hdata_raw'],
                'device'            => $context['device_override'],
            ],
        ];
    }

    /**
     * @param string $reason
     * @return string
     */
    public static function tagReason($reason)
    {
        $reason = trim((string) $reason);
        if ($reason === '') {
            return self::REASON_PREFIX;
        }
        if (strpos($reason, self::REASON_PREFIX) === 0) {
            return $reason;
        }
        return self::REASON_PREFIX . ' ' . $reason;
    }

    /**
     * @param array $input
     * @return array
     */
    private static function normalizeManualInput(array $input)
    {
        $configName = ConfigLoader::sanitizeConfigName($input['config_name'] ?? '');
        if (trim((string) ($input['config_name'] ?? '')) === '') {
            return ['error' => '请选择配置'];
        }

        $domain = self::normalizeHost($input['domain'] ?? '');
        if ($domain === '') {
            $domain = self::defaultHostForConfig($configName);
        }
        if ($domain === '') {
            return ['error' => '请填写域名或绑定站点域名'];
        }

        $pageUrl = trim((string) ($input['page_url'] ?? ''));
        if ($pageUrl === '') {
            $pageUrl = 'https://' . $domain . '/' . $configName . '.php';
        }

        $cookies = [];
        if (!empty($input['cookies_json'])) {
            $decoded = json_decode((string) $input['cookies_json'], true);
            if (!is_array($decoded)) {
                return ['error' => 'Cookie JSON 格式无效'];
            }
            $cookies = $decoded;
        }

        $fpRaw = trim((string) ($input['fp_hdata'] ?? ''));

        return [
            'config_name'       => $configName,
            'ip'                => trim((string) ($input['ip'] ?? '8.8.8.8')),
            'domain'            => $domain,
            'page_url'          => $pageUrl,
            'referer'           => trim((string) ($input['referer'] ?? '')),
            'user_agent'        => trim((string) ($input['user_agent'] ?? 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36')),
            'accept_language'   => trim((string) ($input['accept_language'] ?? 'en-US,en;q=0.9')),
            'visit_md5_code'    => trim((string) ($input['visit_md5_code'] ?? '')),
            'cookies'           => $cookies,
            'cookies_json'      => (string) ($input['cookies_json'] ?? ''),
            'fp_hdata_raw'      => $fpRaw,
            'fp_hdata'          => self::parseFpHdata($fpRaw),
            'device_override'   => trim((string) ($input['device'] ?? '')),
        ];
    }

    /**
     * @param array $row
     * @return array
     */
    private static function contextFromLogRow(array $row)
    {
        $domain   = self::normalizeHost($row['domain'] ?? '');
        $pageUrl  = trim((string) ($row['page'] ?? ''));
        $config   = self::guessConfigName($pageUrl, $domain);

        if ($config === '' && $domain !== '') {
            try {
                $pdo = DomainRepository::pdo();
                DomainRepository::ensureTable($pdo);
                $domRow = DomainRepository::findByHostname($pdo, $domain);
                if ($domRow && !empty($domRow['config_name'])) {
                    $config = trim($domRow['config_name']);
                }
            } catch (Throwable $e) {
            }
        }
        if ($config === '') {
            $config = 'index';
        }

        $referer = trim((string) ($row['http_referer'] ?? ''));
        if ($referer === '') {
            $referer = trim((string) ($row['referer'] ?? ''));
        }

        $visitCode = trim((string) ($row['visit_md5_code'] ?? ''));
        $cookies   = [];
        if ($visitCode !== '' && $visitCode !== 'loss') {
            $cookies['gfuid'] = $visitCode;
        }

        $fpRaw = trim((string) ($row['fp_hdata'] ?? ''));

        return [
            'config_name'       => $config,
            'ip'                => trim((string) ($row['IP'] ?? '')),
            'domain'            => $domain !== '' ? $domain : self::defaultHostForConfig($config),
            'page_url'          => $pageUrl !== '' ? $pageUrl : ('https://' . ($domain ?: 'localhost') . '/' . $config . '.php'),
            'referer'           => $referer,
            'user_agent'        => trim((string) ($row['user_agent'] ?? 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36')),
            'accept_language'   => trim((string) ($row['accept_language_raw'] ?? $row['language'] ?? 'en-US,en;q=0.9')),
            'visit_md5_code'    => $visitCode,
            'cookies'           => $cookies,
            'cookies_json'      => $cookies !== [] ? json_encode($cookies, JSON_UNESCAPED_UNICODE) : '',
            'fp_hdata_raw'      => $fpRaw,
            'fp_hdata'          => self::parseFpHdata($fpRaw),
            'device_override'   => trim((string) ($row['device'] ?? '')),
        ];
    }

    /**
     * @param array      $context
     * @param int|null   $sourceLogId
     * @return array
     */
    private static function execute(array $context, $sourceLogId)
    {
        if (!defined('DB_USERNAME')) {
            require_once dirname(__DIR__, 2) . '/cong.php';
        }
        require_once dirname(__DIR__, 2) . '/lib/bootstrap.php';

        $backup = self::backupGlobals();

        try {
            if (session_status() !== PHP_SESSION_ACTIVE) {
                session_start();
            }
            $_SESSION = [];

            self::injectEnvironment($context);

            $configName = ConfigLoader::loadByName($context['config_name']);

            $GLOBALS['__cloak_simulation_on']  = true;
            $GLOBALS['__cloak_force_sync_log'] = true;
            $GLOBALS['__cloak_debug_on']       = false;

            CloakPipelineTimer::init();

            global $v_info, $reason, $config_name;
            $config_name = $configName;
            $v_info      = new visitorInfo();
            $v_info->get_visitor_Info(COSTM_IP_SCORE, CHECK_KEY, SHOW_SITE_COUNTRY);
            $reason = '';

            CheckPipeline::run();

            if (!isset($_SESSION['check_result']) || $_SESSION['check_result'] === '') {
                $_SESSION['check_result'] = 'false';
                if ($reason === '') {
                    $reason = '未产生判定结果，默认安全页';
                }
            }
            $reason = cloak_finalize_reason($reason, $_SESSION['check_result']);

            $model = self::detectDevice($context);
            $logs  = self::buildLogs($v_info, $reason, $model, $context, $sourceLogId);

            $route = SimulationRouteResolver::dispatch([
                'v_info'      => $v_info,
                'logs'        => $logs,
                'config_name' => $configName,
                'reason'      => $reason,
                'model'       => $model,
            ]);

            $secondary = null;
            if (!empty($route['needs_secondary'])) {
                if (!empty($context['fp_hdata']) && is_array($context['fp_hdata'])) {
                    $secondary = self::runSecondary((int) ($route['log_id'] ?? 0), $context);
                } else {
                    $secondary = [
                        'skipped' => true,
                        'message' => '命中二次风控但未提供 fp_hdata，日志保持 wait 状态',
                    ];
                }
            }

            $timing = CloakPipelineTimer::finish();

            return [
                'ok'          => true,
                'new_log_id'  => $route['log_id'] ?? null,
                'sim_source'  => $sourceLogId,
                'primary'     => [
                    'result' => $_SESSION['check_result'] ?? '',
                    'reason' => $reason,
                    'branch' => $route['branch'] ?? '',
                ],
                'secondary'   => $secondary,
                'timing'      => $timing,
            ];
        } catch (Throwable $e) {
            return [
                'ok'      => false,
                'message' => $e->getMessage(),
            ];
        } finally {
            self::restoreGlobals($backup);
        }
    }

    /**
     * @param int   $logId
     * @param array $context
     * @return array|null
     */
    private static function runSecondary($logId, array $context)
    {
        if ($logId <= 0) {
            return ['ok' => false, 'message' => '无效 log_id，无法执行二次风控'];
        }

        $fp = $context['fp_hdata'];
        if (!is_array($fp)) {
            return ['ok' => false, 'message' => 'fp_hdata 无效'];
        }

        $scheme = 'https://';
        $host   = $context['domain'];
        $fp['log_id']  = $logId;
        $fp['ip']      = $context['ip'];
        $fp['referer'] = $context['referer'];
        if (empty($fp['domain'])) {
            $fp['domain'] = $scheme . $host . '/';
        }

        $result = FCheckHandler::process($fp);
        if (empty($result['ok'])) {
            return [
                'ok'      => false,
                'message' => $result['message'] ?? '二次风控失败',
            ];
        }

        if (!empty($GLOBALS['__cloak_simulation_on']) && isset($result['reason'])) {
            self::tagReasonOnLog($logId, (string) $result['reason']);
        }

        return [
            'ok'     => true,
            'result' => !empty($result['result']),
            'reason' => $result['reason'] ?? '',
            'link'   => $result['link'] ?? '',
        ];
    }

    /**
     * @param int    $logId
     * @param string $reason
     * @param bool   $passed
     */
    private static function tagReasonOnLog($logId, $reason)
    {
        $conn = @new mysqli('localhost', DB_USERNAME, DB_PASSWORD, DB_NAME);
        if (!$conn || $conn->connect_error) {
            return;
        }
        $reasonEsc = cloak_db_escape($conn, self::tagReason($reason));
        $conn->query("UPDATE `visitor_logs` SET `reason`='{$reasonEsc}' WHERE `id`=" . (int) $logId);
        $conn->close();
    }

    /**
     * @param array $context
     */
    private static function injectEnvironment(array $context)
    {
        $host    = $context['domain'];
        $pageUrl = $context['page_url'];
        $parts   = parse_url($pageUrl);
        $path    = $parts['path'] ?? ('/' . $context['config_name'] . '.php');
        $query   = $parts['query'] ?? '';

        $_GET = [];
        if ($query !== '') {
            parse_str($query, $_GET);
        }
        $_REQUEST = $_GET;
        $_COOKIE  = is_array($context['cookies']) ? $context['cookies'] : [];

        $visitCode = $context['visit_md5_code'];
        if ($visitCode === '' || $visitCode === 'loss') {
            $visitCode = md5($context['ip'] . microtime(true));
        }
        $_COOKIE['gfuid']   = $visitCode;
        $_SESSION['gcuid']   = $visitCode;
        $_SESSION['gcu_ip']  = $context['ip'];

        $_SERVER['HTTP_HOST']            = $host;
        $_SERVER['SERVER_NAME']          = $host;
        $_SERVER['REMOTE_ADDR']          = $context['ip'];
        $_SERVER['HTTP_USER_AGENT']      = $context['user_agent'];
        $_SERVER['HTTP_REFERER']         = $context['referer'];
        $_SERVER['HTTP_ACCEPT_LANGUAGE'] = $context['accept_language'];
        $_SERVER['HTTPS']                = 'on';
        $_SERVER['SERVER_PORT']          = '443';
        $_SERVER['REQUEST_URI']          = $path . ($query !== '' ? '?' . $query : '');
        $_SERVER['QUERY_STRING']         = $query;
        $_SERVER['PHP_SELF']             = $path;
        $_SERVER['SCRIPT_NAME']          = $path;
    }

    /**
     * @param visitorInfo $v_info
     * @param string      $reason
     * @param string      $model
     * @param array       $context
     * @param int|null    $sourceLogId
     * @return array
     */
    private static function buildLogs($v_info, $reason, $model, array $context, $sourceLogId)
    {
        $logs = [
            'site_name'       => str_replace('www.', '', $context['domain']),
            'customers_ip'    => $v_info->v_ip,
            'country'         => substr((string) $v_info->v_country, 0, 2),
            'v_Browser'       => $v_info->v_Browser,
            'v_referer'       => $v_info->v_referer,
            'Client'          => $v_info->v_Client,
            'v_PageURL'       => $v_info->v_curPageURL,
            'accept_language' => $v_info->accept_language,
            'visit_md5_code'  => $_SESSION['gcuid'] ?? $v_info->visit_md5_code,
            'campagin_id'     => $v_info->costm_ip_score,
            'result'          => $_SESSION['check_result'] ?? 'false',
            'device'          => $model,
            'reason'          => self::tagReason($reason),
            'user_agent'      => $context['user_agent'],
            'http_referer'    => $context['referer'],
            'accept_language_raw' => $context['accept_language'],
            'is_simulation'   => 1,
        ];
        if ($sourceLogId) {
            $logs['sim_source_log_id'] = (int) $sourceLogId;
        }
        return $logs;
    }

    /**
     * @param array $context
     * @return string
     */
    private static function detectDevice(array $context)
    {
        if ($context['device_override'] !== '') {
            return $context['device_override'];
        }
        $uaPath = dirname(__DIR__, 2) . '/Mobile-Detect/src/MobileDetect.php';
        if (!is_file($uaPath)) {
            return 'PC';
        }
        require_once $uaPath;
        $detect     = new \Detection\MobileDetect;
        $deviceType = ($detect->isMobile() ? ($detect->isTablet() ? 'tablet' : 'phone') : 'computer');
        if ($deviceType === 'computer') {
            return 'PC';
        }
        $patterns = $deviceType === 'tablet' ? $detect::getTabletDevices() : $detect::getPhoneDevices();
        $model    = ucfirst($deviceType);
        foreach ($patterns as $brand => $pattern) {
            if (preg_match('/' . $pattern . '/i', $context['user_agent'], $matches)) {
                $model = $brand . ' ' . (isset($matches[1]) ? $matches[1] : '');
                break;
            }
        }
        return trim($model) !== '' ? trim($model) : 'Mobile';
    }

    /**
     * @param string $raw
     * @return array|null
     */
    private static function parseFpHdata($raw)
    {
        $raw = trim((string) $raw);
        if ($raw === '') {
            return null;
        }
        if ($raw[0] === '{' || $raw[0] === '[') {
            $decoded = json_decode($raw, true);
            return is_array($decoded) ? $decoded : null;
        }
        $decodedString = base64_decode($raw, true);
        if ($decodedString === false) {
            return null;
        }
        $decoded = json_decode($decodedString, true);
        return is_array($decoded) ? $decoded : null;
    }

    /**
     * @param string $pageUrl
     * @param string $domain
     * @return string
     */
    private static function guessConfigName($pageUrl, $domain)
    {
        $path = parse_url($pageUrl, PHP_URL_PATH);
        if (is_string($path) && $path !== '') {
            $base = basename($path, '.php');
            if ($base !== '' && preg_match('/^[a-zA-Z0-9_-]+$/', $base)) {
                return $base;
            }
        }
        return '';
    }

    /**
     * @param string $configName
     * @return string
     */
    private static function defaultHostForConfig($configName)
    {
        try {
            $pdo = DomainRepository::pdo();
            DomainRepository::ensureTable($pdo);
            $host = DomainRepository::primaryHostnameForConfig($pdo, $configName);
            if ($host !== '') {
                return self::normalizeHost($host);
            }
        } catch (Throwable $e) {
        }
        return self::normalizeHost($_SERVER['HTTP_HOST'] ?? 'localhost');
    }

    /**
     * @param string $host
     * @return string
     */
    private static function normalizeHost($host)
    {
        $host = trim(strtolower((string) $host));
        $host = preg_replace('#^https?://#', '', $host);
        $host = explode('/', $host)[0];
        return preg_replace('/^www\./', '', $host);
    }

    /**
     * @param int $logId
     * @return array|null
     */
    private static function fetchLogRow($logId)
    {
        if ($logId <= 0) {
            return null;
        }
        $conn = @new mysqli('localhost', DB_USERNAME, DB_PASSWORD, DB_NAME);
        if (!$conn || $conn->connect_error) {
            return null;
        }
        VisitorLogSchema::ensureColumns($conn);
        $sql = 'SELECT * FROM `visitor_logs` WHERE `id`=' . (int) $logId . ' LIMIT 1';
        $res = $conn->query($sql);
        if (!$res || $res->num_rows === 0) {
            $conn->close();
            return null;
        }
        $row = $res->fetch_assoc();
        $res->free();
        $conn->close();
        return $row;
    }

    /**
     * @return array
     */
    private static function backupGlobals()
    {
        return [
            '_SERVER'  => $_SERVER,
            '_GET'     => $_GET,
            '_REQUEST' => $_REQUEST,
            '_COOKIE'  => $_COOKIE,
            '_SESSION' => $_SESSION,
            'GLOBALS'  => [
                '__cloak_simulation_on'  => $GLOBALS['__cloak_simulation_on'] ?? null,
                '__cloak_force_sync_log' => $GLOBALS['__cloak_force_sync_log'] ?? null,
                '__cloak_debug_on'       => $GLOBALS['__cloak_debug_on'] ?? null,
            ],
        ];
    }

    /**
     * @param array $backup
     */
    private static function restoreGlobals(array $backup)
    {
        $_SERVER  = $backup['_SERVER'];
        $_GET     = $backup['_GET'];
        $_REQUEST = $backup['_REQUEST'];
        $_COOKIE  = $backup['_COOKIE'];
        $_SESSION = $backup['_SESSION'];
        foreach ($backup['GLOBALS'] as $key => $value) {
            if ($value === null) {
                unset($GLOBALS[$key]);
            } else {
                $GLOBALS[$key] = $value;
            }
        }
    }
}
