<?php

namespace WPSN\Modules\Redirects;

defined('ABSPATH') || exit;

/**
 * Regex helpers for advanced redirect rules.
 *
 * Regex redirects are intentionally opt-in and stricter than exact/prefix rules:
 *  - the pattern must be compilable
 *  - the pattern must be anchored to the path start (^/...)
 *  - backreference usage in the destination must not exceed the capture count
 */
final class RedirectRegex
{
    /**
     * @return array{ok: bool, message: string, compiled: string, warnings: array<int, string>, capture_count: int}
     */
    public static function validateRule(string $pattern, string $destination, int $statusCode): array
    {
        $result = self::validatePattern($pattern);
        if (!$result['ok']) {
            return $result;
        }

        if (!in_array($statusCode, [410, 451], true)) {
            $maxBackref = self::maxReplacementBackreference($destination);
            if ($maxBackref > 0 && $maxBackref > $result['capture_count']) {
                return [
                    'ok' => false,
                    'message' => sprintf(
                        /* translators: 1: highest referenced capture group, 2: available capture groups */
                        __('Destination uses capture group %1$d, but the regex only defines %2$d capturing group(s).', 'wp-seo-next'),
                        $maxBackref,
                        $result['capture_count']
                    ),
                    'compiled' => '',
                    'warnings' => [],
                    'capture_count' => $result['capture_count'],
                ];
            }
        }

        return $result;
    }

    /**
     * @return array{ok: bool, message: string, compiled: string, warnings: array<int, string>, capture_count: int}
     */
    public static function validatePattern(string $pattern): array
    {
        $pattern = self::normalizeStoredPattern($pattern);
        if ($pattern === '') {
            return [
                'ok' => false,
                'message' => __('Regex pattern is empty.', 'wp-seo-next'),
                'compiled' => '',
                'warnings' => [],
                'capture_count' => 0,
            ];
        }

        if (strlen($pattern) > 255) {
            return [
                'ok' => false,
                'message' => __('Regex pattern is too long. Keep it under 255 characters.', 'wp-seo-next'),
                'compiled' => '',
                'warnings' => [],
                'capture_count' => 0,
            ];
        }

        if (preg_match('/[\r\n\0]/', $pattern)) {
            return [
                'ok' => false,
                'message' => __('Regex pattern contains invalid control characters.', 'wp-seo-next'),
                'compiled' => '',
                'warnings' => [],
                'capture_count' => 0,
            ];
        }

        $body = self::patternBody($pattern);
        if ($body === '' || strpos(ltrim($body), '^/') !== 0) {
            return [
                'ok' => false,
                'message' => __('Regex redirects must start at the path root, for example ^/old/(.+)$.', 'wp-seo-next'),
                'compiled' => '',
                'warnings' => [],
                'capture_count' => 0,
            ];
        }

        if (preg_match('/\(\?(?:R|0|[1-9-])/', $body) || strpos($body, '\\g{') !== false || strpos($body, '\\g-') !== false) {
            return [
                'ok' => false,
                'message' => __('Recursive or subroutine regex patterns are not allowed in redirects.', 'wp-seo-next'),
                'compiled' => '',
                'warnings' => [],
                'capture_count' => 0,
            ];
        }

        $compiled = self::preparePattern($pattern);
        if ($compiled === '') {
            return [
                'ok' => false,
                'message' => __('Regex pattern could not be prepared.', 'wp-seo-next'),
                'compiled' => '',
                'warnings' => [],
                'capture_count' => 0,
            ];
        }

        $probe = @preg_match($compiled, '/__wpsn_regex_probe__');
        if ($probe === false) {
            $msg = function_exists('preg_last_error_msg') ? (string) preg_last_error_msg() : __('Invalid regex pattern.', 'wp-seo-next');
            if ($msg === '') {
                $msg = __('Invalid regex pattern.', 'wp-seo-next');
            }

            return [
                'ok' => false,
                'message' => $msg,
                'compiled' => '',
                'warnings' => [],
                'capture_count' => 0,
            ];
        }

        $captureCount = self::countCapturingGroups($body);
        if ($captureCount > 9) {
            return [
                'ok' => false,
                'message' => __('Regex redirects currently support up to 9 capturing groups.', 'wp-seo-next'),
                'compiled' => '',
                'warnings' => [],
                'capture_count' => $captureCount,
            ];
        }

        $warnings = [];
        if (!self::looksEndAnchored($body)) {
            $warnings[] = __('Pattern is not end-anchored with $, so it may match more URLs than expected.', 'wp-seo-next');
        }

        if (@preg_match($compiled, '/') === 1 && $body !== '^/$') {
            $warnings[] = __('Pattern also matches the homepage /. Double-check that this is intentional.', 'wp-seo-next');
        }

        return [
            'ok' => true,
            'message' => '',
            'compiled' => $compiled,
            'warnings' => $warnings,
            'capture_count' => $captureCount,
        ];
    }

    public static function matches(string $pattern, string $path): bool
    {
        $validation = self::validatePattern($pattern);
        if (!$validation['ok']) {
            return false;
        }

        return @preg_match($validation['compiled'], $path) === 1;
    }

    public static function applyReplacement(string $pattern, string $replacement, string $path): string
    {
        $validation = self::validatePattern($pattern);
        if (!$validation['ok']) {
            return '';
        }

        $result = @preg_replace($validation['compiled'], $replacement, $path, 1);
        return is_string($result) ? $result : '';
    }

    public static function normalizeStoredPattern(string $pattern): string
    {
        return trim($pattern);
    }

    public static function maxReplacementBackreference(string $replacement): int
    {
        $replacement = (string) $replacement;
        if ($replacement === '') {
            return 0;
        }

        $max = 0;
        if (preg_match_all('/(?<!\\\\)(?:\\$([1-9][0-9]?)|\\\\([1-9][0-9]?))/', $replacement, $m)) {
            foreach ([1, 2] as $idx) {
                if (empty($m[$idx]) || !is_array($m[$idx])) {
                    continue;
                }
                foreach ($m[$idx] as $n) {
                    $num = (int) $n;
                    if ($num > $max) {
                        $max = $num;
                    }
                }
            }
        }

        return $max;
    }

    private static function preparePattern(string $pattern): string
    {
        $pattern = self::normalizeStoredPattern($pattern);
        if ($pattern === '') {
            return '';
        }

        $parsed = self::parseDelimitedPattern($pattern);
        if (is_array($parsed)) {
            $modifiers = (string) ($parsed['modifiers'] ?? '');
            if (strpos($modifiers, 'u') === false) {
                $modifiers .= 'u';
            }

            return (string) $parsed['delimiter'] . (string) $parsed['body'] . (string) $parsed['delimiter'] . $modifiers;
        }

        return '#' . str_replace('#', '\\#', $pattern) . '#u';
    }

    private static function patternBody(string $pattern): string
    {
        $parsed = self::parseDelimitedPattern($pattern);
        if (is_array($parsed)) {
            return (string) ($parsed['body'] ?? '');
        }

        return self::normalizeStoredPattern($pattern);
    }

    /**
     * @return array{delimiter: string, body: string, modifiers: string}|null
     */
    private static function parseDelimitedPattern(string $pattern): ?array
    {
        $pattern = trim($pattern);
        if ($pattern === '') {
            return null;
        }

        $delimiter = $pattern[0];
        if (preg_match('/[[:alnum:]\\s\\\\]/', $delimiter)) {
            return null;
        }

        $len = strlen($pattern);
        for ($i = 1; $i < $len; $i++) {
            if ($pattern[$i] === '\\') {
                $i++;
                continue;
            }

            if ($pattern[$i] !== $delimiter) {
                continue;
            }

            $modifiers = substr($pattern, $i + 1);
            if ($modifiers !== '' && !preg_match('/^[imsxuADSUXJu]*$/', $modifiers)) {
                continue;
            }

            return [
                'delimiter' => $delimiter,
                'body' => substr($pattern, 1, $i - 1),
                'modifiers' => $modifiers,
            ];
        }

        return null;
    }

    private static function countCapturingGroups(string $body): int
    {
        if ($body === '') {
            return 0;
        }

        $count = 0;
        $len = strlen($body);
        for ($i = 0; $i < $len; $i++) {
            if ($body[$i] !== '(') {
                continue;
            }

            if ($i > 0 && $body[$i - 1] === '\\') {
                continue;
            }

            $next = substr($body, $i + 1, 1);
            if ($next !== '?') {
                $count++;
                continue;
            }

            $sig2 = substr($body, $i + 1, 2);
            $sig3 = substr($body, $i + 1, 3);
            if (in_array($sig2, ['?:', '?=', '?!', '?>'], true) || in_array($sig3, ['?<=', '?<!', '?#'], true)) {
                continue;
            }

            if ($sig3 === '?P<' || $sig2 === '?<' || $sig2 === "?'") {
                $count++;
            }
        }

        return $count;
    }

    private static function looksEndAnchored(string $body): bool
    {
        $body = rtrim($body);
        if ($body === '') {
            return false;
        }

        $len = strlen($body);
        $escapes = 0;
        for ($i = $len - 2; $i >= 0 && $body[$i] === '\\'; $i--) {
            $escapes++;
        }

        return $body[$len - 1] === '$' && ($escapes % 2 === 0);
    }
}
