<?php

namespace VulnTitan\Services;

class FluentFormSpamService
{
    protected const TOKEN_FIELD = '_vulntitan_ff_token';
    protected const HONEYPOT_FIELD = '_vulntitan_ff_hp';
    protected const RATE_LIMIT_PREFIX = 'vulntitan_fluentforms_rate_';
    protected const MIN_TOKEN_AGE = 1;
    protected const MAX_TOKEN_AGE = 2 * HOUR_IN_SECONDS;
    protected const MAX_TOKEN_FUTURE_SKEW = 5 * MINUTE_IN_SECONDS;
    protected const BLOCK_MESSAGE = 'Your submission was blocked because the system detected spam or abuse. Remove excessive links and try again.';

    protected static bool $booted = false;

    /**
     * @var array<string,bool>
     */
    protected static array $loggedDecisions = [];

    public static function boot(): void
    {
        if (self::$booted) {
            return;
        }

        self::$booted = true;

        if (!self::isFluentFormsAvailable()) {
            return;
        }

        add_action('wp_ajax_nopriv_fluentform_submit', [__CLASS__, 'interceptAjaxSubmit'], 1);
        add_action('wp_ajax_fluentform_submit', [__CLASS__, 'interceptAjaxSubmit'], 1);
        add_action('fluentform/form_element_start', [__CLASS__, 'renderProtectionFields']);
        add_filter('fluentform/validation_errors', [__CLASS__, 'detectValidationErrors'], 20, 4);
        add_action('fluentform/before_insert_submission', [__CLASS__, 'detectSpam'], 10, 3);
    }

    public static function interceptAjaxSubmit(): void
    {
        if (!self::isEnabled()) {
            return;
        }

        $rawData = isset($_POST['data']) ? wp_unslash($_POST['data']) : '';
        $formId = isset($_POST['form_id']) ? (int) wp_unslash($_POST['form_id']) : 0;
        $requestData = [];

        if (is_string($rawData) && $rawData !== '') {
            parse_str($rawData, $requestData);
        }

        if (!is_array($requestData) || self::shouldBypass()) {
            return;
        }

        $form = self::resolveFormObject($formId);
        if (!self::isValidForm($form)) {
            return;
        }

        $decision = self::evaluateSubmission($requestData, $form);
        if ($decision === null) {
            return;
        }

        self::logDecision($requestData, $form, $decision);

        wp_send_json(
            [
                'errors' => esc_html__(self::BLOCK_MESSAGE, 'vulntitan'),
            ],
            422
        );
    }

    /**
     * @param mixed $form
     */
    public static function renderProtectionFields($form): void
    {
        if (!self::isEnabled() || !self::isValidForm($form)) {
            return;
        }

        if (function_exists('esc_attr')) {
            echo '<input type="hidden" name="' . esc_attr(self::TOKEN_FIELD) . '" value="' . esc_attr(self::buildFormToken(self::resolveFormId($form))) . '">';
            echo '<div class="ff-el-group ff_force_hide vulntitan-ff-honeypot" aria-hidden="true">';
            echo '<label for="vulntitan-ff-hp">' . esc_html__('Leave this field empty', 'vulntitan') . '</label>';
            echo '<input type="text" id="vulntitan-ff-hp" name="' . esc_attr(self::HONEYPOT_FIELD) . '" value="" tabindex="-1" autocomplete="off" inputmode="text">';
            echo '</div>';
        }
    }

    /**
     * @param mixed $insertData
     * @param mixed $requestData
     * @param mixed $form
     */
    public static function detectSpam($insertData, $requestData, $form): void
    {
        if (!self::isEnabled() || !self::isValidForm($form)) {
            return;
        }

        if (!is_array($requestData)) {
            $requestData = [];
        }

        if (self::shouldBypass()) {
            return;
        }

        $decision = self::evaluateSubmission($requestData, $form);
        if ($decision === null) {
            return;
        }

        self::logDecision($requestData, $form, $decision);

        wp_send_json(
            [
                'errors' => esc_html__(self::BLOCK_MESSAGE, 'vulntitan'),
            ],
            422
        );
    }

    /**
     * @param mixed $errors
     * @param mixed $formData
     * @param mixed $form
     * @param mixed $fields
     * @return array<string,mixed>
     */
    public static function detectValidationErrors($errors, $formData, $form, $fields): array
    {
        if (!is_array($errors)) {
            $errors = [];
        }

        if (!self::isEnabled() || !self::isValidForm($form) || !is_array($formData)) {
            return $errors;
        }

        if (self::shouldBypass()) {
            return $errors;
        }

        $decision = self::evaluateSubmission($formData, $form);
        if ($decision === null) {
            return $errors;
        }

        self::logDecision($formData, $form, $decision);

        $fieldName = self::resolveErrorFieldName($formData, is_array($fields) ? $fields : []);
        if (!isset($errors[$fieldName]) || !is_array($errors[$fieldName])) {
            $errors[$fieldName] = [];
        }

        array_unshift($errors[$fieldName], esc_html__(self::BLOCK_MESSAGE, 'vulntitan'));

        return $errors;
    }

    /**
     * @param array<string,mixed> $requestData
     * @param object $form
     * @return array<string,mixed>|null
     */
    protected static function evaluateSubmission(array $requestData, object $form): ?array
    {
        $settings = FirewallService::getSettings();
        $formId = self::resolveFormId($form);
        $ipAddress = self::resolveIpAddress();
        $content = self::flattenPostedContent($requestData);
        $normalizedContent = self::normalizeContent($content);
        $linkCount = self::countLinks($content);
        $linkDomains = self::extractLinkDomains($content);
        [$topDomain, $topDomainHits] = self::findTopDomain($linkDomains);
        $visitorKey = self::buildVisitorKey($formId, $requestData, $ipAddress);
        $rateWindowMinutes = max(1, (int) ($settings['submission_rate_limit_window_minutes'] ?? $settings['comment_rate_limit_window_minutes'] ?? 10));
        $rateCount = self::incrementRateCounter($visitorKey, $rateWindowMinutes);
        $maxAttempts = max(1, (int) ($settings['submission_rate_limit_attempts'] ?? $settings['comment_rate_limit_attempts'] ?? 3));

        if ($rateCount > $maxAttempts) {
            return self::buildDecision(
                'fluentforms_rate_limit',
                __('Fluent Forms submission rate limit exceeded.', 'vulntitan'),
                [
                    'form_id' => $formId,
                    'rate_count' => $rateCount,
                    'rate_limit' => $maxAttempts,
                    'window_minutes' => $rateWindowMinutes,
                ],
                [
                    'content_hash' => self::buildContentHash($normalizedContent),
                    'link_count' => $linkCount,
                ]
            );
        }

        $honeypotValue = self::readRequestValue($requestData, self::HONEYPOT_FIELD);
        if ($honeypotValue !== '') {
            return self::buildDecision(
                'fluentforms_honeypot',
                __('Blocked Fluent Forms submission because the hidden honeypot field was filled.', 'vulntitan'),
                [
                    'form_id' => $formId,
                    'rate_count' => $rateCount,
                    'window_minutes' => $rateWindowMinutes,
                ],
                [
                    'content_hash' => self::buildContentHash($normalizedContent),
                ]
            );
        }

        $tokenValidation = self::validateFormToken(self::readRequestValue($requestData, self::TOKEN_FIELD), $formId);
        if (!empty($tokenValidation['present']) && empty($tokenValidation['valid'])) {
            return self::buildDecision(
                'fluentforms_invalid_token',
                __('Fluent Forms token validation failed.', 'vulntitan'),
                [
                    'form_id' => $formId,
                    'rate_count' => $rateCount,
                    'window_minutes' => $rateWindowMinutes,
                ],
                [
                    'content_hash' => self::buildContentHash($normalizedContent),
                ]
            );
        }

        $minSeconds = max(self::MIN_TOKEN_AGE, (int) ($settings['submission_min_submit_seconds'] ?? $settings['comment_min_submit_seconds'] ?? 3));
        $tokenAge = (int) ($tokenValidation['age'] ?? 0);
        if (!empty($tokenValidation['valid']) && $tokenAge > 0 && $tokenAge < $minSeconds) {
            return self::buildDecision(
                'fluentforms_submitted_too_fast',
                __('Fluent Forms submission was sent faster than the configured minimum time threshold.', 'vulntitan'),
                [
                    'form_id' => $formId,
                    'rate_count' => $rateCount,
                    'window_minutes' => $rateWindowMinutes,
                    'submitted_after_seconds' => $tokenAge,
                    'minimum_seconds' => $minSeconds,
                ],
                [
                    'content_hash' => self::buildContentHash($normalizedContent),
                    'link_count' => $linkCount,
                ]
            );
        }

        $maxLinks = max(0, (int) ($settings['submission_max_links'] ?? $settings['comment_max_links'] ?? 2));
        if ($linkCount > $maxLinks) {
            return self::buildDecision(
                'fluentforms_too_many_links',
                __('Fluent Forms submission contains more links than the configured threshold.', 'vulntitan'),
                [
                    'form_id' => $formId,
                    'link_count' => $linkCount,
                    'max_links' => $maxLinks,
                ],
                [
                    'content_hash' => self::buildContentHash($normalizedContent),
                    'link_count' => $linkCount,
                    'top_link_domain' => $topDomain,
                    'top_link_domain_hits' => $topDomainHits,
                ]
            );
        }

        if ($topDomain !== '' && $topDomainHits >= 3) {
            return self::buildDecision(
                'fluentforms_repeated_link_domain',
                __('Fluent Forms submission repeats the same linked domain multiple times.', 'vulntitan'),
                [
                    'form_id' => $formId,
                    'top_link_domain' => $topDomain,
                    'top_link_domain_hits' => $topDomainHits,
                ],
                [
                    'content_hash' => self::buildContentHash($normalizedContent),
                    'link_count' => $linkCount,
                    'top_link_domain' => $topDomain,
                    'top_link_domain_hits' => $topDomainHits,
                ]
            );
        }

        return null;
    }

    protected static function buildDecision(string $ruleId, string $reason, array $details, array $context): array
    {
        return [
            'rule_group' => 'form_spam',
            'rule_id' => $ruleId,
            'reason' => $reason,
            'details' => $details,
            'context' => $context,
        ];
    }

    /**
     * @param array<string,mixed> $requestData
     * @param object $form
     * @param array<string,mixed> $decision
     */
    protected static function logDecision(array $requestData, object $form, array $decision): void
    {
        $decisionKey = self::buildDecisionKey($requestData, $form, $decision);
        if (isset(self::$loggedDecisions[$decisionKey])) {
            return;
        }

        self::$loggedDecisions[$decisionKey] = true;

        $context = $decision['context'] ?? [];

        if (!is_array($context)) {
            $context = [];
        }

        $context['form_id'] = self::resolveFormId($form);
        $context['form_title'] = self::resolveFormTitle($form);
        $context['field_names'] = array_values(array_map('strval', array_keys($requestData)));
        $context['source_url'] = self::resolveSourceUrl();
        $context['embedded_post_id'] = (int) self::readRequestValue($requestData, '__fluent_form_embded_post_id');

        FirewallService::logEvent('request_blocked', [
            'event_source' => 'fluent_forms',
            'event_action' => 'block',
            'blocked' => 1,
            'response_code' => 403,
            'severity' => 4,
            'ip_address' => self::resolveIpAddress(),
            'username' => self::resolveUsername($requestData),
            'request_method' => strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'POST')),
            'request_uri' => (string) ($_SERVER['REQUEST_URI'] ?? ''),
            'request_path' => (string) parse_url((string) ($_SERVER['REQUEST_URI'] ?? ''), PHP_URL_PATH),
            'rule_group' => (string) ($decision['rule_group'] ?? 'form_spam'),
            'rule_id' => (string) ($decision['rule_id'] ?? ''),
            'reason' => (string) ($decision['reason'] ?? ''),
            'details' => is_array($decision['details'] ?? null) ? $decision['details'] : [],
            'context' => $context,
        ]);
    }

    protected static function isEnabled(): bool
    {
        $settings = FirewallService::getSettings();

        return !empty($settings['form_shield_enabled']) && !empty($settings['form_provider_fluentforms_enabled']);
    }

    protected static function isFluentFormsAvailable(): bool
    {
        return defined('FLUENTFORM') && class_exists('\FluentForm\App\Modules\SubmissionHandler\SubmissionHandler');
    }

    /**
     * @param mixed $form
     */
    protected static function isValidForm($form): bool
    {
        return is_object($form) && property_exists($form, 'id');
    }

    protected static function shouldBypass(): bool
    {
        return function_exists('current_user_can') && current_user_can('moderate_comments');
    }

    /**
     * @param object $form
     */
    protected static function resolveFormId(object $form): int
    {
        return isset($form->id) ? (int) $form->id : 0;
    }

    /**
     * @param object $form
     */
    protected static function resolveFormTitle(object $form): string
    {
        if (!isset($form->title)) {
            return '';
        }

        return sanitize_text_field((string) $form->title);
    }

    protected static function resolveIpAddress(): string
    {
        return FirewallService::detectClientIp();
    }

    /**
     * @param array<string,mixed> $requestData
     */
    protected static function flattenPostedContent(array $requestData): string
    {
        $parts = [];

        foreach ($requestData as $key => $value) {
            if (!is_string($key) || self::shouldIgnoreField($key)) {
                continue;
            }

            foreach (self::flattenValue($value) as $item) {
                if ($item !== '') {
                    $parts[] = $item;
                }
            }
        }

        return implode("\n", $parts);
    }

    /**
     * @param mixed $value
     * @return array<int,string>
     */
    protected static function flattenValue($value): array
    {
        if (is_array($value)) {
            $flattened = [];

            foreach ($value as $item) {
                foreach (self::flattenValue($item) as $nestedItem) {
                    $flattened[] = $nestedItem;
                }
            }

            return $flattened;
        }

        if ($value === null || is_object($value)) {
            return [];
        }

        return [trim((string) $value)];
    }

    protected static function shouldIgnoreField(string $key): bool
    {
        if (in_array($key, [self::TOKEN_FIELD, self::HONEYPOT_FIELD, 'action', 'form_id'], true)) {
            return true;
        }

        if (strpos($key, '__fluent_') === 0) {
            return true;
        }

        return strpos($key, 'fluentformnonce') !== false;
    }

    protected static function normalizeContent(string $content): string
    {
        $content = wp_strip_all_tags($content);
        $content = strtolower(trim($content));
        $content = preg_replace('/\s+/u', ' ', $content);

        return is_string($content) ? $content : '';
    }

    protected static function buildContentHash(string $normalizedContent): string
    {
        if ($normalizedContent === '') {
            return '';
        }

        return sha1($normalizedContent);
    }

    protected static function countLinks(string $content): int
    {
        if ($content === '') {
            return 0;
        }

        preg_match_all('/https?:\/\/|www\./i', $content, $matches);

        return is_array($matches[0] ?? null) ? count($matches[0]) : 0;
    }

    /**
     * @return array<string,int>
     */
    protected static function extractLinkDomains(string $content): array
    {
        if ($content === '') {
            return [];
        }

        preg_match_all('~https?://[^\s<>"\']+|www\.[^\s<>"\']+~iu', $content, $matches);
        $domains = [];

        foreach ((array) ($matches[0] ?? []) as $match) {
            $candidate = stripos($match, 'www.') === 0 ? 'http://' . $match : $match;
            $host = strtolower((string) parse_url($candidate, PHP_URL_HOST));

            if ($host === '') {
                continue;
            }

            if (strpos($host, 'www.') === 0) {
                $host = substr($host, 4);
            }

            if ($host === '') {
                continue;
            }

            if (!isset($domains[$host])) {
                $domains[$host] = 0;
            }

            $domains[$host]++;
        }

        return $domains;
    }

    /**
     * @param array<string,int> $domains
     * @return array{0:string,1:int}
     */
    protected static function findTopDomain(array $domains): array
    {
        if (!$domains) {
            return ['', 0];
        }

        arsort($domains, SORT_NUMERIC);
        $domain = (string) key($domains);
        $hits = (int) reset($domains);

        return [$domain, $hits];
    }

    /**
     * @param array<string,mixed> $requestData
     */
    protected static function buildVisitorKey(int $formId, array $requestData, string $ipAddress): string
    {
        $email = '';
        foreach ($requestData as $key => $value) {
            if (!is_string($key) || self::shouldIgnoreField($key)) {
                continue;
            }

            if (stripos($key, 'mail') === false && stripos($key, 'email') === false) {
                continue;
            }

            $candidate = is_array($value) ? reset($value) : $value;
            if (is_scalar($candidate) || $candidate === null) {
                $email = strtolower(trim((string) $candidate));
                break;
            }
        }

        $parts = [
            $formId,
            $ipAddress,
            strtolower(trim((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''))),
            $email,
        ];

        return sha1(implode('|', $parts));
    }

    protected static function incrementRateCounter(string $visitorKey, int $windowMinutes): int
    {
        $transientKey = self::RATE_LIMIT_PREFIX . substr($visitorKey, 0, 40);
        $data = get_transient($transientKey);

        if (!is_array($data)) {
            $data = [
                'count' => 0,
                'started_at' => time(),
            ];
        }

        $data['count'] = max(0, (int) ($data['count'] ?? 0)) + 1;
        $data['started_at'] = (int) ($data['started_at'] ?? time());

        set_transient($transientKey, $data, max(1, $windowMinutes) * MINUTE_IN_SECONDS);

        return (int) $data['count'];
    }

    protected static function buildFormToken(int $formId): string
    {
        $timestamp = time();
        $payload = $timestamp . '|' . max(0, $formId);
        $signature = wp_hash($payload . '|' . self::TOKEN_FIELD);

        return $payload . '|' . $signature;
    }

    protected static function validateFormToken(string $rawToken, int $formId): array
    {
        $token = trim($rawToken);
        if ($token === '') {
            return [
                'present' => false,
                'valid' => false,
                'age' => 0,
            ];
        }

        $parts = explode('|', $token, 3);
        if (count($parts) !== 3) {
            return [
                'present' => true,
                'valid' => false,
                'age' => 0,
            ];
        }

        [$timestampRaw, $formIdRaw, $signature] = $parts;
        $timestamp = (int) $timestampRaw;
        $tokenFormId = (int) $formIdRaw;
        $expected = wp_hash($timestampRaw . '|' . $formIdRaw . '|' . self::TOKEN_FIELD);
        $now = time();
        $age = max(0, $now - $timestamp);

        if ($timestamp <= 0 || $tokenFormId < 0 || !hash_equals($expected, $signature)) {
            return [
                'present' => true,
                'valid' => false,
                'age' => 0,
            ];
        }

        if ($formId > 0 && $tokenFormId > 0 && $formId !== $tokenFormId) {
            return [
                'present' => true,
                'valid' => false,
                'age' => 0,
            ];
        }

        if ($timestamp > ($now + self::MAX_TOKEN_FUTURE_SKEW) || $age > self::getTokenMaxAge($formId)) {
            return [
                'present' => true,
                'valid' => false,
                'age' => $age,
            ];
        }

        return [
            'present' => true,
            'valid' => true,
            'age' => $age,
        ];
    }

    protected static function getTokenMaxAge(int $formId): int
    {
        $maxAge = (int) apply_filters('vulntitan_form_token_max_age', self::MAX_TOKEN_AGE, 'fluent_forms', $formId);

        return max(self::MIN_TOKEN_AGE, $maxAge);
    }

    /**
     * @param array<string,mixed> $requestData
     */
    protected static function readRequestValue(array $requestData, string $field): string
    {
        if (!array_key_exists($field, $requestData)) {
            return '';
        }

        $value = $requestData[$field];
        if (!is_scalar($value) && $value !== null) {
            return '';
        }

        return trim((string) $value);
    }

    /**
     * @param array<string,mixed> $requestData
     * @param object $form
     * @param array<string,mixed> $decision
     */
    protected static function buildDecisionKey(array $requestData, object $form, array $decision): string
    {
        $contentHash = self::buildContentHash(self::normalizeContent(self::flattenPostedContent($requestData)));
        $ruleId = (string) ($decision['rule_id'] ?? '');

        return sha1(self::resolveFormId($form) . '|' . $contentHash . '|' . $ruleId . '|' . self::resolveIpAddress());
    }

    /**
     * @param array<string,mixed> $requestData
     */
    protected static function resolveUsername(array $requestData): string
    {
        foreach ($requestData as $key => $value) {
            $normalizedKey = strtolower((string) $key);
            if (self::shouldIgnoreField($normalizedKey) || strpos($normalizedKey, 'name') === false) {
                continue;
            }

            $candidate = is_array($value) ? reset($value) : $value;
            if (!is_scalar($candidate) && $candidate !== null) {
                continue;
            }

            $username = sanitize_user((string) $candidate, true);
            if ($username !== '') {
                return $username;
            }
        }

        return '';
    }

    protected static function resolveSourceUrl(): string
    {
        $referer = wp_get_referer();
        if (is_string($referer) && $referer !== '') {
            return $referer;
        }

        return home_url((string) ($_SERVER['REQUEST_URI'] ?? '/'));
    }

    /**
     * @return object
     */
    protected static function resolveFormObject(int $formId): object
    {
        if (class_exists('\FluentForm\App\Models\Form') && method_exists('\FluentForm\App\Models\Form', 'find')) {
            $form = \FluentForm\App\Models\Form::find($formId);
            if (is_object($form) && property_exists($form, 'id')) {
                return $form;
            }
        }

        return (object) [
            'id' => $formId,
            'title' => '',
        ];
    }

    /**
     * @param array<string,mixed> $formData
     * @param array<string,mixed> $fields
     */
    protected static function resolveErrorFieldName(array $formData, array $fields): string
    {
        foreach (['message', 'email', 'subject'] as $preferredField) {
            if (array_key_exists($preferredField, $formData) || array_key_exists($preferredField, $fields)) {
                return $preferredField;
            }
        }

        foreach (array_keys($fields) as $fieldName) {
            if (is_string($fieldName) && !self::shouldIgnoreField($fieldName)) {
                return $fieldName;
            }
        }

        foreach (array_keys($formData) as $fieldName) {
            if (is_string($fieldName) && !self::shouldIgnoreField($fieldName)) {
                return $fieldName;
            }
        }

        return 'message';
    }
}
