<?php

namespace App\Support;

class BackUrl
{
    /**
     * back 参数安全护栏（用于 Blade 中 `{!! !!}` 输出的 href 场景）：
     * - 仅允许站内相对路径（/ 开头）
     * - 拒绝引号/尖括号（防属性注入/XSS）
     * - 拒绝 nested back=（防 URL 膨胀/绕过）
     */
    public static function sanitizeForLinks(string $incomingBack): string
    {
        $incomingBack = (string) $incomingBack;

        if ($incomingBack === '') {
            return '';
        }

        // 长度安全阀：避免超长 back 造成 header/url 过大、日志污染或潜在 DoS
        // 与表单侧 max:2000 的校验口径对齐。
        if (strlen($incomingBack) > 2000) {
            return '';
        }

        if (!str_starts_with($incomingBack, '/')) {
            return '';
        }

        // 拒绝协议相对 URL（例如 //evil.com），避免 open redirect
        if (str_starts_with($incomingBack, '//')) {
            return '';
        }

        if (preg_match('/["\'<>]/', $incomingBack)) {
            return '';
        }

        // 拒绝控制字符/CRLF 注入（包括明文与常见 URL 编码形式）
        if (preg_match('/[\r\n\t]/', $incomingBack)) {
            return '';
        }
        if (preg_match('/%0d|%0a|%09/i', $incomingBack)) {
            return '';
        }

        // 拒绝 back 自身再包含 back=（避免无限嵌套导致 URL 膨胀，且容易绕过页面侧护栏）
        // 同时拒绝“二次编码”的 back%3D（例如 %2526back%253D 经过一次 urldecode 后变成 %26back%3D，
        // 在浏览器点击后会再次被解码为 &back=，形成绕过）。
        if (preg_match('/(?:^|[?&])back=/', $incomingBack)) {
            return '';
        }
        if (preg_match('/(?:^|[?&]|%26|%3f)back%3d/i', $incomingBack)) {
            return '';
        }
        if (preg_match('/back%253d/i', $incomingBack)) {
            return '';
        }

        return $incomingBack;
    }

    /**
     * 安全版“保留当前 query 并覆盖字段”的站内相对链接构造器。
     *
     * 典型用途：列表页里的各种「统计卡/治理入口/快捷链接」需要：
     * - 保留当前筛选条件
     * - 覆盖指定字段
     * - 并且 back 只能保留通过 sanitizeForLinks 的安全值（否则移除）
     */
    public static function currentPathWithQuery(array $overrides = [], string $safeBackForLinks = ''): string
    {
        $q = request()->query();

        if ($safeBackForLinks !== '') {
            $q['back'] = $safeBackForLinks;
        } else {
            unset($q['back']);
        }

        foreach ($overrides as $k => $v) {
            if ($v === null) {
                unset($q[$k]);
            } else {
                $q[$k] = $v;
            }
        }

        $url = '/' . ltrim(request()->path(), '/');
        if (count($q) > 0) {
            $url .= '?' . \Illuminate\Support\Arr::query($q);
        }

        return $url;
    }

    /**
     * 合并 query 到当前 path（不自动处理 back），用于“保留筛选上下文但不透传 back”的场景。
     *
     * @param array $baseQuery 例如 request()->query() 并移除 back 后的数组
     * @param array $overrides 覆盖字段（null 表示删除）
     */
    public static function mergeQueryToCurrentPath(array $baseQuery, array $overrides = []): string
    {
        $q = $baseQuery;

        foreach ($overrides as $k => $v) {
            if ($v === null) {
                unset($q[$k]);
            } else {
                $q[$k] = $v;
            }
        }

        $url = '/' . ltrim(request()->path(), '/');
        if (count($q) > 0) {
            $url .= '?' . \Illuminate\Support\Arr::query($q);
        }

        return $url;
    }

    /**
     * 给指定站内相对路径附加安全 back（若 back 为空则原样返回）。
     *
     * 用途：列表页“全部/返回来源”等需要清空筛选但保留 back 的场景。
     */
    public static function withBack(string $path, string $safeBackForLinks = ''): string
    {
        return self::appendBack($path, $safeBackForLinks, false);
    }

    /**
     * withBack 的变体：将 back 放到 query 的最前面（用于少数对 query 顺序有严格断言的页面/测试）。
     */
    public static function withBackFirst(string $path, string $safeBackForLinks = ''): string
    {
        return self::appendBack($path, $safeBackForLinks, true);
    }

    private static function appendBack(string $path, string $safeBackForLinks = '', bool $preferFirst = false): string
    {
        $path = (string) $path;

        if ($safeBackForLinks === '') {
            return $path;
        }

        // 防御性：即使调用方声称传入的是 safeBack，这里也再按统一口径过滤一遍。
        // 若不合法，则不拼接 back（避免注入/嵌套 back= 膨胀/外链等风险）。
        $safeBackForLinks = self::sanitizeForLinks((string) $safeBackForLinks);
        if ($safeBackForLinks === '') {
            return $path;
        }

        // 仅支持站内相对路径；若调用方传入了异常值，这里不做拼接，直接返回原 path。
        if ($path === '' || !str_starts_with($path, '/')) {
            return $path;
        }

        // 兼容：若调用方传入的 path 自带 fragment（#xxx），这里拆出并在最后追加。
        // fragment 仍做白名单校验（与 withBackAndFragment 同口径），避免意外注入/属性污染。
        $fragmentSuffix = '';
        if (str_contains($path, '#')) {
            [$path, $fragment] = explode('#', $path, 2);
            $fragment = ltrim((string) $fragment, '#');
            if ($fragment !== '' && preg_match('/^[A-Za-z0-9_-]+$/', $fragment)) {
                $fragmentSuffix = '#' . $fragment;
            }
        }

        $backQuery = \Illuminate\Support\Arr::query(['back' => $safeBackForLinks]);

        if (!str_contains($path, '?')) {
            return $path . '?' . $backQuery . $fragmentSuffix;
        }

        [$base, $qs] = explode('?', $path, 2);
        $qs = trim((string) $qs);
        $qs = trim($qs, "& ");

        // 处理类似 "/xx?" 或 "/xx?&" 的情况：视为无 query
        if ($qs === '') {
            return $base . '?' . $backQuery . $fragmentSuffix;
        }

        // 若 path 自身已包含 back=（调用方误用），则不再追加，避免重复 back 造成 URL 膨胀/绕过。
        if (preg_match('/(?:^|&)back=/', $qs)) {
            return $base . '?' . $qs . $fragmentSuffix;
        }

        if ($preferFirst) {
            return $base . '?' . $backQuery . '&' . $qs . $fragmentSuffix;
        }

        return $base . '?' . $qs . '&' . $backQuery . $fragmentSuffix;
    }

    /**
     * 给指定站内相对路径附加安全 back，并可选追加锚点（fragment）。
     *
     * 说明：fragment 仅允许 [A-Za-z0-9_-]，不符合则直接丢弃 fragment。
     * 典型用途：列表页行级「去补回执 / 去补退款」等链接，需要跳转到详情页某个区块。
     */
    public static function withBackAndFragment(string $path, string $safeBackForLinks = '', string $fragment = ''): string
    {
        // 若调用方传入了 path 自带 fragment，同时又传入 fragment 参数，避免出现 "#old#new"。
        if ((string) $fragment !== '' && str_contains((string) $path, '#')) {
            [$path] = explode('#', (string) $path, 2);
        }

        $url = self::withBack($path, $safeBackForLinks);

        $fragment = ltrim((string) $fragment, '#');
        if ($fragment === '') {
            return $url;
        }

        if (!preg_match('/^[A-Za-z0-9_-]+$/', $fragment)) {
            return $url;
        }

        return $url . '#' . $fragment;
    }

    /**
     * withBackFirst + fragment 的组合：用于少数对 query 顺序有严格断言的页面/测试。
     */
    public static function withBackFirstAndFragment(string $path, string $safeBackForLinks = '', string $fragment = ''): string
    {
        // 若调用方传入了 path 自带 fragment，同时又传入 fragment 参数，避免出现 "#old#new"。
        if ((string) $fragment !== '' && str_contains((string) $path, '#')) {
            [$path] = explode('#', (string) $path, 2);
        }

        $url = self::withBackFirst($path, $safeBackForLinks);

        $fragment = ltrim((string) $fragment, '#');
        if ($fragment === '') {
            return $url;
        }

        if (!preg_match('/^[A-Za-z0-9_-]+$/', $fragment)) {
            return $url;
        }

        return $url . '#' . $fragment;
    }

    /**
     * 当前路径下的“快捷筛选”链接构造器：
     * - 仅保留指定上下文键（例如 merchant_id/plan_id/keyword/lead_id 等）
     * - 覆盖 overrides（null 表示移除）
     * - 强制清空 page（避免落到空页）
     * - back 仅保留安全值（由调用方传入 sanitizeForLinks 产物）
     */
    public static function currentPathQuickFilter(array $contextKeys, array $overrides = [], string $safeBackForLinks = ''): string
    {
        $path = '/' . ltrim(request()->path(), '/');

        $contextMap = [];
        foreach ($contextKeys as $k) {
            $contextMap[(string) $k] = 1;
        }

        $q = array_intersect_key(request()->query(), $contextMap);

        if ($safeBackForLinks !== '') {
            $q['back'] = $safeBackForLinks;
        } else {
            unset($q['back']);
        }

        // 快捷筛选不应继承分页
        unset($q['page']);

        foreach ($overrides as $k => $v) {
            if ($v === null) {
                unset($q[$k]);
                continue;
            }
            $q[$k] = $v;
        }

        if (count($q) === 0) {
            return $path;
        }

        return $path . '?' . \Illuminate\Support\Arr::query($q);
    }

    /**
     * 生成当前页面去掉 back 后的 selfWithoutBack（站内相对路径）。
     * 用于：生成返回来源 back 或防止 back 嵌套膨胀。
     */
    public static function selfWithoutBack(): string
    {
        $currentQuery = request()->query();
        unset($currentQuery['back']);

        $url = '/' . ltrim(request()->path(), '/');
        if (count($currentQuery) > 0) {
            $url .= '?' . \Illuminate\Support\Arr::query($currentQuery);
        }

        return $url;
    }
}
