<?php

/**
 * Клас для безпеки
 * Захист від XSS, CSRF, SQL ін'єкцій та інших атак
 *
 * @package Flowaxy\Infrastructure\Security
 * @version 1.0.0 Alpha prerelease
 */

declare(strict_types=1);

namespace Flowaxy\Infrastructure\Security;

use Flowaxy\Infrastructure\Security\Session as SecuritySession;
use Flowaxy\Infrastructure\Security\Hash;
use Flowaxy\Support\Facades\Session;
use Flowaxy\Support\Facades\Log;
use Flowaxy\Support\Helpers\SanitizationHelper;
use Flowaxy\Infrastructure\Security\RequestFilter;

// Імпорт вбудованих функцій PHP для оптимізації компілятора та чистого коду
use function array_filter;
use function array_map;
use function array_values;
use function basename;
use function class_exists;
use function count;
use function explode;
use function filter_var;
use function htmlspecialchars;
use function is_array;
use function is_numeric;
use function is_string;
use function mb_strlen;
use function mb_substr;
use function md5;
use function pathinfo;
use function preg_replace;
use function session_id;
use function session_status;
use function sprintf;
use function str_contains;
use function str_replace;
use function strlen;
use function strtolower;
use function strtr;
use function strip_tags;
use function time;
use function trim;

/**
 * Клас Security надає методи для захисту від різних типів атак
 *
 * @method static bool isValidEmail(string $email)
 * @method static bool isValidUrl(string $url)
 * @method static bool isValidIp(string $ip, bool $ipv6 = true)
 * @method static string getClientIp()
 */
class Security
{
    /** @var array<int, string> */
    private const array IP_HEADERS = [
        'HTTP_CF_CONNECTING_IP',
        'HTTP_CLIENT_IP',
        'HTTP_X_FORWARDED_FOR',
        'HTTP_X_FORWARDED',
        'HTTP_X_CLUSTER_CLIENT_IP',
        'HTTP_FORWARDED_FOR',
        'HTTP_FORWARDED',
        'REMOTE_ADDR',
    ];

    private const string CSRF_TOKEN_KEY = 'csrf_token';
    private const int CSRF_TOKEN_LENGTH = 32;
    private const int DEFAULT_RATE_LIMIT_ATTEMPTS = 5;
    private const int DEFAULT_RATE_LIMIT_TIME = 900; // 15 минут

    /**
     * Отримує менеджер сесії з перевіркою доступності
     *
     * @return \Flowaxy\Support\Managers\SessionManager|null
     */
    private static function getSessionManager(): ?\Flowaxy\Support\Managers\SessionManager
    {
        // class_exists() автоматично викликає autoloader в PHP 8.4+
        if (!class_exists(Session::class)) {
            Log::error('Security: Session facade class not found');
            return null;
        }

        return Session::manager();
    }

    /**
     * Забезпечує активну сесію
     */
    private static function ensureActiveSession(): void
    {
        if (!SecuritySession::isStarted()) {
            SecuritySession::start();
        }

        if (session_status() !== PHP_SESSION_ACTIVE) {
            Log::warning('Security: Session is not active after start attempt', [
                'session_status' => session_status(),
            ]);
            SecuritySession::start();
        }
    }

    /**
     * Очищення даних від XSS
     *
     * @param mixed $data Дані для очищення
     * @param bool $strict Строгий режим (видаляти HTML теги)
     * @return mixed
     */
    public static function clean(mixed $data, bool $strict = false): mixed
    {
        if (is_array($data)) {
            return array_map(fn($item) => self::clean($item, $strict), $data);
        }

        if (is_string($data)) {
            return $strict ? strip_tags($data) : htmlspecialchars($data, ENT_QUOTES | ENT_HTML5, 'UTF-8');
        }

        return $data;
    }

    /**
     * Розширена санітизація вводу з валідацією типів даних
     *
     * @param mixed $data Дані для санітизації
     * @param string $type Тип даних (string, int, float, email, url, html)
     * @param array<string, mixed> $options Опції валідації
     * @return mixed Санітизовані дані
     */
    public static function sanitize(mixed $data, string $type = 'string', array $options = []): mixed
    {
        if (is_array($data)) {
            return SanitizationHelper::sanitizeArray($data);
        }

        $allowedTags = $options['allowed_tags'] ?? '<p><br><strong><em><ul><ol><li><a>';
        $stripTags = $options['strip_tags'] ?? true;

        return match ($type) {
            'int', 'integer' => SanitizationHelper::sanitizeInt($data),
            'float', 'double' => SanitizationHelper::sanitizeFloat($data),
            'email' => SanitizationHelper::sanitizeEmail((string)$data),
            'url' => SanitizationHelper::sanitizeUrl((string)$data),
            'html' => strip_tags((string)$data, $allowedTags),
            'string' => SanitizationHelper::sanitizeString((string)$data, $stripTags),
            default => SanitizationHelper::sanitizeString((string)$data, $stripTags),
        };
    }

    /**
     * Валідація даних за правилами
     *
     * @param mixed $data Дані для валідації
     * @param array<string, mixed> $rules Правила валідації
     * @return array<string, string> Помилки валідації (порожній масив якщо валідація пройшла)
     */
    public static function validate(mixed $data, array $rules): array
    {
        $errors = [];

        foreach ($rules as $field => $ruleSet) {
            $value = is_array($data) ? ($data[$field] ?? null) : $data;
            $ruleArray = is_array($ruleSet) ? $ruleSet : explode('|', (string)$ruleSet);

            foreach ($ruleArray as $rule) {
                $ruleParts = explode(':', (string)$rule);
                $ruleName = $ruleParts[0];
                $ruleValue = $ruleParts[1] ?? null;

                match ($ruleName) {
                    'required' => self::validateRequired($value, $field, $errors),
                    'email' => self::validateEmail($value, $field, $errors),
                    'url' => self::validateUrl($value, $field, $errors),
                    'min' => self::validateMin($value, $field, $ruleValue, $errors),
                    'max' => self::validateMax($value, $field, $ruleValue, $errors),
                    'numeric' => self::validateNumeric($value, $field, $errors),
                    default => null,
                };
            }
        }

        return $errors;
    }

    /**
     * Генерація CSRF токена
     *
     * @return string
     */
    public static function csrfToken(): string
    {
        self::ensureActiveSession();

        $session = self::getSessionManager();
        if ($session === null) {
            return '';
        }

        // Скидаємо префікс для CSRF токена
        $originalPrefix = $session->getPrefix();
        if ($originalPrefix !== '') {
            $session->setPrefix('');
        }

        try {
            if (!$session->has(self::CSRF_TOKEN_KEY)) {
                $token = Hash::token(self::CSRF_TOKEN_LENGTH);
                $session->set(self::CSRF_TOKEN_KEY, $token);

                Log::debug('Security::csrfToken: Generated new CSRF token', [
                    'session_id' => session_id(),
                    'token_length' => strlen($token),
                ]);
            }

            $token = $session->get(self::CSRF_TOKEN_KEY);

            // Якщо токен порожній, генеруємо новий
            if (empty($token)) {
                $token = Hash::token(self::CSRF_TOKEN_LENGTH);
                $session->set(self::CSRF_TOKEN_KEY, $token);

                Log::warning('Security::csrfToken: Token was empty, generated new one', [
                    'session_id' => session_id(),
                ]);
            }

            return (string)$token;
        } finally {
            // Відновлюємо оригінальний префікс
            if ($originalPrefix !== '') {
                $session->setPrefix($originalPrefix);
            }
        }
    }

    /**
     * Перевірка CSRF токена
     *
     * @param string|null $token Токен для перевірки (якщо null, береться з POST/GET)
     * @return bool
     */
    public static function verifyCsrfToken(?string $token = null): bool
    {
        self::ensureActiveSession();

        $session = self::getSessionManager();
        if ($session === null) {
            return false;
        }

        // Скидаємо префікс для CSRF токена
        $originalPrefix = $session->getPrefix();
        if ($originalPrefix !== '') {
            $session->setPrefix('');
        }

        try {
            $sessionToken = $session->get(self::CSRF_TOKEN_KEY);

            if (empty($sessionToken)) {
                Log::warning('Security::verifyCsrfToken: Session token is empty', [
                    'session_id' => session_id(),
                ]);
                return false;
            }

            // Отримуємо токен з запиту, якщо не передано
            if ($token === null) {
                $token = RequestFilter::post(
                    self::CSRF_TOKEN_KEY,
                    RequestFilter::get(self::CSRF_TOKEN_KEY, '', 'string'),
                    'string'
                );
            }

            if (empty($token)) {
                Log::warning('Security::verifyCsrfToken: Token from request is empty', [
                    'session_id' => session_id(),
                ]);
                return false;
            }

            // Нормалізуємо токени до рядків
            $sessionToken = (string)$sessionToken;
            $token = (string)$token;

            // Порівнюємо токени
            $result = ($sessionToken === $token) || Hash::equals($sessionToken, $token);

            if (!$result) {
                Log::warning('Security::verifyCsrfToken: CSRF token mismatch', [
                    'session_id' => session_id(),
                ]);
            }

            return $result;
        } finally {
            // Відновлюємо оригінальний префікс
            if ($originalPrefix !== '') {
                $session->setPrefix($originalPrefix);
            }
        }
    }

    /**
     * Генерація CSRF токена для форми
     *
     * @return string HTML input з токеном
     */
    public static function csrfField(): string
    {
        $token = self::csrfToken();
        return sprintf(
            '<input type="hidden" name="%s" value="%s">',
            self::CSRF_TOKEN_KEY,
            htmlspecialchars($token, ENT_QUOTES, 'UTF-8')
        );
    }

    /**
     * Санітизація рядка для SQL (використовуйте підготовлені запити!)
     *
     * @param string $string Рядок для санітизації
     * @return string
     * @deprecated Використовуйте підготовлені запити замість цього
     */
    public static function sql(string $string): string
    {
        return str_replace(
            ['\\', "\n", "\r", "\x00", "\x1a", "'", '"'],
            ['\\\\', '\\n', '\\r', '\\0', '\\Z', "\\'", '\\"'],
            $string
        );
    }

    /**
     * Валідація email
     *
     * @param string $email Email для перевірки
     * @return bool
     */
    public static function isValidEmail(string $email): bool
    {
        return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
    }

    /**
     * Валідація URL
     *
     * @param string $url URL для перевірки
     * @return bool
     */
    public static function isValidUrl(string $url): bool
    {
        return filter_var($url, FILTER_VALIDATE_URL) !== false;
    }

    /**
     * Валідація IP адреси
     *
     * @param string $ip IP адреса
     * @param bool $ipv6 Дозволяти IPv6
     * @return bool
     */
    public static function isValidIp(string $ip, bool $ipv6 = true): bool
    {
        $flags = $ipv6 ? (FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6) : FILTER_FLAG_IPV4;
        return filter_var($ip, FILTER_VALIDATE_IP, $flags) !== false;
    }

    /**
     * Отримання IP адреси клієнта
     *
     * @return string
     */
    public static function getClientIp(): string
    {
        foreach (self::IP_HEADERS as $header) {
            $ip = RequestFilter::server($header, '', 'string');
            if (empty($ip)) {
                continue;
            }

            // Обробка кількох IP адрес (беремо перший)
            if (str_contains($ip, ',')) {
                $ip = trim(explode(',', $ip)[0]);
            }

            if (self::isValidIp($ip)) {
                return $ip;
            }
        }

        return RequestFilter::server('REMOTE_ADDR', '0.0.0.0', 'string');
    }

    /**
     * Перевірка, чи є запит AJAX
     *
     * @return bool
     */
    public static function isAjax(): bool
    {
        $requestedWith = RequestFilter::server('HTTP_X_REQUESTED_WITH', '', 'string');
        return !empty($requestedWith) && strtolower($requestedWith) === 'xmlhttprequest';
    }

    /**
     * Генерація безпечного випадкового імені файла
     *
     * @param string $filename Оригінальне ім'я файла
     * @return string
     */
    public static function sanitizeFilename(string $filename): string
    {
        $filename = basename($filename);
        $filename = self::transliterate($filename);
        $filename = preg_replace('/[^a-zA-Z0-9._-]/', '_', $filename) ?? $filename;

        if (strlen($filename) > 255) {
            $ext = pathinfo($filename, PATHINFO_EXTENSION);
            $name = pathinfo($filename, PATHINFO_FILENAME);
            $maxNameLength = 255 - mb_strlen($ext) - 1;
            $filename = mb_substr($name, 0, $maxNameLength) . '.' . $ext;
        }

        return $filename;
    }

    /**
     * Транскрипція кирилиці в латиницю
     *
     * @param string $text Текст для транскрипції
     * @return string
     */
    public static function transliterate(string $text): string
    {
        $translitMap = [
            'а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd',
            'е' => 'e', 'ё' => 'yo', 'ж' => 'zh', 'з' => 'z', 'и' => 'i',
            'й' => 'y', 'к' => 'k', 'л' => 'l', 'м' => 'm', 'н' => 'n',
            'о' => 'o', 'п' => 'p', 'р' => 'r', 'с' => 's', 'т' => 't',
            'у' => 'u', 'ф' => 'f', 'х' => 'h', 'ц' => 'ts', 'ч' => 'ch',
            'ш' => 'sh', 'щ' => 'sch', 'ъ' => '', 'ы' => 'y', 'ь' => '',
            'э' => 'e', 'ю' => 'yu', 'я' => 'ya',
            'А' => 'A', 'Б' => 'B', 'В' => 'V', 'Г' => 'G', 'Д' => 'D',
            'Е' => 'E', 'Ё' => 'Yo', 'Ж' => 'Zh', 'З' => 'Z', 'И' => 'I',
            'Й' => 'Y', 'К' => 'K', 'Л' => 'L', 'М' => 'M', 'Н' => 'N',
            'О' => 'O', 'П' => 'P', 'Р' => 'R', 'С' => 'S', 'Т' => 'T',
            'У' => 'U', 'Ф' => 'F', 'Х' => 'H', 'Ц' => 'Ts', 'Ч' => 'Ch',
            'Ш' => 'Sh', 'Щ' => 'Sch', 'Ъ' => '', 'Ы' => 'Y', 'Ь' => '',
            'Э' => 'E', 'Ю' => 'Yu', 'Я' => 'Ya',
            'ґ' => 'g', 'Ґ' => 'G', 'і' => 'i', 'І' => 'I', 'ї' => 'yi', 'Ї' => 'Yi',
            'є' => 'ye', 'Є' => 'Ye',
        ];

        return strtr($text, $translitMap);
    }

    /**
     * Захист від брутфорсу (обмеження спроб)
     *
     * @param string $key Ключ для відстеження (наприклад, IP або email)
     * @param int $maxAttempts Максимальна кількість спроб
     * @param int $lockoutTime Час блокування в секундах
     * @return bool True якщо досягнуто ліміт
     */
    public static function isRateLimited(
        string $key,
        int $maxAttempts = self::DEFAULT_RATE_LIMIT_ATTEMPTS,
        int $lockoutTime = self::DEFAULT_RATE_LIMIT_TIME
    ): bool {
        $session = self::getSessionManager();
        if ($session === null) {
            return false;
        }

        $attemptsKey = 'rate_limit_' . md5($key);
        $attempts = $session->get($attemptsKey, []);
        $now = time();

        // Фільтруємо застарілі спроби
        $attempts = array_filter(
            $attempts,
            fn($timestamp) => ($now - (int)$timestamp) < $lockoutTime
        );

        if (count($attempts) >= $maxAttempts) {
            return true;
        }

        $attempts[] = $now;
        $session->set($attemptsKey, array_values($attempts));

        return false;
    }

    /**
     * Скидання лічильника спроб
     *
     * @param string $key Ключ
     */
    public static function resetRateLimit(string $key): void
    {
        $session = self::getSessionManager();
        if ($session === null) {
            return;
        }

        $session->remove('rate_limit_' . md5($key));
    }

    // ============================================================================
    // Приватні методи валідації
    // ============================================================================

    /**
     * Валідація обов'язкового поля
     */
    private static function validateRequired(mixed $value, string $field, array &$errors): void
    {
        if (empty($value) && $value !== '0') {
            $errors[$field] = "Поле {$field} обов'язкове";
        }
    }

    /**
     * Валідація email
     */
    private static function validateEmail(mixed $value, string $field, array &$errors): void
    {
        if (!empty($value) && !self::isValidEmail((string)$value)) {
            $errors[$field] = "Поле {$field} має бути валідною email адресою";
        }
    }

    /**
     * Валідація URL
     */
    private static function validateUrl(mixed $value, string $field, array &$errors): void
    {
        if (!empty($value) && !self::isValidUrl((string)$value)) {
            $errors[$field] = "Поле {$field} має бути валідною URL адресою";
        }
    }

    /**
     * Валідація мінімальної довжини
     */
    private static function validateMin(mixed $value, string $field, ?string $ruleValue, array &$errors): void
    {
        if (!empty($value) && $ruleValue !== null && strlen((string)$value) < (int)$ruleValue) {
            $errors[$field] = "Поле {$field} має містити мінімум {$ruleValue} символів";
        }
    }

    /**
     * Валідація максимальної довжини
     */
    private static function validateMax(mixed $value, string $field, ?string $ruleValue, array &$errors): void
    {
        if (!empty($value) && $ruleValue !== null && strlen((string)$value) > (int)$ruleValue) {
            $errors[$field] = "Поле {$field} має містити максимум {$ruleValue} символів";
        }
    }

    /**
     * Валідація числового значення
     */
    private static function validateNumeric(mixed $value, string $field, array &$errors): void
    {
        if (!empty($value) && !is_numeric($value)) {
            $errors[$field] = "Поле {$field} має бути числом";
        }
    }

    // ============================================================================
    // Алиаси для RequestFilter - безпечний доступ до суперглобальних змінних
    // ============================================================================

    /**
     * Безпечне отримання значення з $_GET
     *
     * @param string $key Ключ параметра
     * @param mixed $default Значення за замовчуванням
     * @param string $type Тип даних (string, int, float, email, url, array)
     * @return mixed Відфільтроване значення
     */
    public static function get(string $key, mixed $default = null, string $type = 'string'): mixed
    {
        return RequestFilter::get($key, $default, $type);
    }

    /**
     * Безпечне отримання значення з $_POST
     *
     * @param string $key Ключ параметра
     * @param mixed $default Значення за замовчуванням
     * @param string $type Тип даних (string, int, float, email, url, array)
     * @return mixed Відфільтроване значення
     */
    public static function post(string $key, mixed $default = null, string $type = 'string'): mixed
    {
        return RequestFilter::post($key, $default, $type);
    }

    /**
     * Безпечне отримання значення з $_REQUEST
     *
     * @param string $key Ключ параметра
     * @param mixed $default Значення за замовчуванням
     * @param string $type Тип даних (string, int, float, email, url, array)
     * @return mixed Відфільтроване значення
     */
    public static function request(string $key, mixed $default = null, string $type = 'string'): mixed
    {
        return RequestFilter::request($key, $default, $type);
    }

    /**
     * Безпечне отримання значення з $_SERVER
     *
     * @param string $key Ключ параметра
     * @param mixed $default Значення за замовчуванням
     * @param string $type Тип даних (string, int, float, email, url, array)
     * @return mixed Відфільтроване значення
     */
    public static function server(string $key, mixed $default = null, string $type = 'string'): mixed
    {
        return RequestFilter::server($key, $default, $type);
    }

    /**
     * Безпечне отримання значення з $_COOKIE
     *
     * @param string $key Ключ параметра
     * @param mixed $default Значення за замовчуванням
     * @param string $type Тип даних (string, int, float, email, url, array)
     * @return mixed Відфільтроване значення
     */
    public static function cookie(string $key, mixed $default = null, string $type = 'string'): mixed
    {
        return RequestFilter::cookie($key, $default, $type);
    }

    /**
     * Отримати всі відфільтровані GET параметри
     *
     * @return array<string, mixed>
     */
    public static function allGet(): array
    {
        return RequestFilter::allGet();
    }

    /**
     * Отримати всі відфільтровані POST параметри
     *
     * @return array<string, mixed>
     */
    public static function allPost(): array
    {
        return RequestFilter::allPost();
    }

    /**
     * Отримати всі відфільтровані SERVER змінні
     *
     * @return array<string, mixed>
     */
    public static function allServer(): array
    {
        return RequestFilter::allServer();
    }
}
