<?php

namespace VulnTitan\Services;

class ContactFormSpamService
{
    protected const TOKEN_FIELD = '_vulntitan_cf7_token';
    protected const HONEYPOT_FIELD = '_vulntitan_cf7_hp';
    protected const RATE_LIMIT_PREFIX = 'vulntitan_cf7_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 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::isContactForm7Available()) {
            return;
        }

        add_filter('wpcf7_form_hidden_fields', [__CLASS__, 'addHiddenFields']);
        add_filter('wpcf7_form_elements', [__CLASS__, 'appendHoneypotField']);
        add_filter('wpcf7_spam', [__CLASS__, 'detectSpam'], 8, 2);
    }

    public static function addHiddenFields(array $fields): array
    {
        if (!self::isEnabled()) {
            return $fields;
        }

        $fields[self::TOKEN_FIELD] = self::buildFormToken(self::getCurrentFormId());

        return $fields;
    }

    public static function appendHoneypotField(string $html): string
    {
        if (!self::isEnabled()) {
            return $html;
        }

        if (strpos($html, 'name="' . self::HONEYPOT_FIELD . '"') !== false) {
            return $html;
        }

        $honeypot = '<span class="vulntitan-cf7-honeypot" style="position:absolute;left:-9999px;top:auto;width:1px;height:1px;overflow:hidden;" aria-hidden="true">'
            . '<label for="vulntitan-cf7-hp">' . esc_html__('Leave this field empty', 'vulntitan') . '</label>'
            . '<input type="text" id="vulntitan-cf7-hp" name="' . esc_attr(self::HONEYPOT_FIELD) . '" value="" tabindex="-1" autocomplete="off" inputmode="text">'
            . '</span>';

        return $html . "\n" . $honeypot;
    }

    /**
     * @param bool $spam
     * @param mixed $submission
     */
    public static function detectSpam($spam, $submission): bool
    {
        if ($spam || !self::isEnabled() || !self::isValidSubmission($submission)) {
            return (bool) $spam;
        }

        if (self::shouldBypass($submission)) {
            return false;
        }

        $decision = self::evaluateSubmission($submission);
        if ($decision === null) {
            return false;
        }

        $reason = (string) ($decision['reason'] ?? __('Blocked contact form submission by anti-spam protection.', 'vulntitan'));
        $submission->add_spam_log([
            'agent' => 'vulntitan',
            'reason' => $reason,
        ]);

        self::logDecision($submission, $decision);

        return true;
    }

    /**
     * @param \WPCF7_Submission $submission
     * @return array<string,mixed>|null
     */
    protected static function evaluateSubmission(\WPCF7_Submission $submission): ?array
    {
        $settings = FirewallService::getSettings();
        $formId = self::resolveFormId($submission);
        $ipAddress = self::resolveIpAddress($submission);
        $postedData = self::getPostedData($submission);
        $content = self::flattenPostedContent($postedData);
        $normalizedContent = self::normalizeContent($content);
        $linkCount = self::countLinks($content);
        $linkDomains = self::extractLinkDomains($content);
        [$topDomain, $topDomainHits] = self::findTopDomain($linkDomains);
        $visitorKey = self::buildVisitorKey($submission, $postedData, $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(
                'cf7_rate_limit',
                __('Contact form 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::readRequestField(self::HONEYPOT_FIELD);
        if ($honeypotValue !== '') {
            return self::buildDecision(
                'cf7_honeypot',
                __('Blocked contact form 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::readRequestField(self::TOKEN_FIELD), $formId);
        if (!empty($tokenValidation['present']) && empty($tokenValidation['valid'])) {
            return self::buildDecision(
                'cf7_invalid_token',
                __('Contact form 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(
                'cf7_submitted_too_fast',
                __('Contact form was submitted 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(
                'cf7_too_many_links',
                __('Contact form 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(
                'cf7_repeated_link_domain',
                __('Contact form 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' => 'contact_form_spam',
            'rule_id' => $ruleId,
            'reason' => $reason,
            'details' => $details,
            'context' => $context,
        ];
    }

    protected static function logDecision(\WPCF7_Submission $submission, array $decision): void
    {
        $decisionKey = self::buildDecisionKey($submission, $decision);
        if (isset(self::$loggedDecisions[$decisionKey])) {
            return;
        }

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

        $contactForm = $submission->get_contact_form();
        $postedData = self::getPostedData($submission);
        $context = $decision['context'] ?? [];

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

        $context['contact_form_id'] = self::resolveFormId($submission);
        $context['contact_form_title'] = is_object($contactForm) && method_exists($contactForm, 'title')
            ? (string) $contactForm->title()
            : '';
        $context['field_names'] = array_values(array_map('strval', array_keys($postedData)));
        $context['unit_tag'] = (string) $submission->get_meta('unit_tag');
        $context['source_url'] = (string) $submission->get_meta('url');

        FirewallService::logEvent('request_blocked', [
            'event_source' => 'contact_form_7',
            'event_action' => 'block',
            'blocked' => 1,
            'response_code' => 403,
            'severity' => 4,
            'ip_address' => self::resolveIpAddress($submission),
            'username' => self::resolveUsername($submission, $postedData),
            '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'] ?? 'contact_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_cf7_enabled']);
    }

    protected static function isContactForm7Available(): bool
    {
        return class_exists('\WPCF7_Submission') && class_exists('\WPCF7_ContactForm');
    }

    /**
     * @param mixed $submission
     */
    protected static function isValidSubmission($submission): bool
    {
        return $submission instanceof \WPCF7_Submission;
    }

    protected static function shouldBypass(\WPCF7_Submission $submission): bool
    {
        if (function_exists('current_user_can') && current_user_can('moderate_comments')) {
            return true;
        }

        $userId = (int) $submission->get_meta('current_user_id');
        if ($userId <= 0 || !function_exists('get_userdata')) {
            return false;
        }

        $user = get_userdata($userId);

        return $user instanceof \WP_User && $user->has_cap('moderate_comments');
    }

    protected static function getCurrentFormId(): int
    {
        if (!class_exists('\WPCF7_ContactForm') || !method_exists('\WPCF7_ContactForm', 'get_current')) {
            return 0;
        }

        $contactForm = \WPCF7_ContactForm::get_current();

        return is_object($contactForm) && method_exists($contactForm, 'id')
            ? (int) $contactForm->id()
            : 0;
    }

    protected static function resolveFormId(\WPCF7_Submission $submission): int
    {
        $contactForm = $submission->get_contact_form();

        return is_object($contactForm) && method_exists($contactForm, 'id')
            ? (int) $contactForm->id()
            : 0;
    }

    protected static function resolveIpAddress(\WPCF7_Submission $submission): string
    {
        $ipAddress = trim((string) $submission->get_meta('remote_ip'));
        if ($ipAddress !== '') {
            return $ipAddress;
        }

        return FirewallService::detectClientIp();
    }

    protected static function getPostedData(\WPCF7_Submission $submission): array
    {
        $postedData = $submission->get_posted_data();

        return is_array($postedData) ? $postedData : [];
    }

    protected static function flattenPostedContent(array $postedData): string
    {
        $parts = [];

        foreach ($postedData as $key => $value) {
            if (!is_string($key) || in_array($key, [self::TOKEN_FIELD, self::HONEYPOT_FIELD], true)) {
                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 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];
    }

    protected static function buildVisitorKey(\WPCF7_Submission $submission, array $postedData, string $ipAddress): string
    {
        $email = '';
        foreach ($postedData as $key => $value) {
            if (stripos((string) $key, 'mail') === false && stripos((string) $key, 'email') === false) {
                continue;
            }

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

        $parts = [
            self::resolveFormId($submission),
            $ipAddress,
            strtolower(trim((string) $submission->get_meta('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, 'contact_form_7', $formId);

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

    protected static function readRequestField(string $field): string
    {
        if (!isset($_REQUEST[$field])) {
            return '';
        }

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

        return trim((string) $value);
    }

    protected static function buildDecisionKey(\WPCF7_Submission $submission, array $decision): string
    {
        $postedDataHash = (string) $submission->get_posted_data_hash();
        $ruleId = (string) ($decision['rule_id'] ?? '');

        return sha1(self::resolveFormId($submission) . '|' . $postedDataHash . '|' . $ruleId);
    }

    protected static function resolveUsername(\WPCF7_Submission $submission, array $postedData): string
    {
        $userId = (int) $submission->get_meta('current_user_id');
        if ($userId > 0 && function_exists('get_userdata')) {
            $user = get_userdata($userId);
            if ($user instanceof \WP_User) {
                return sanitize_user((string) $user->user_login, true);
            }
        }

        foreach ($postedData as $key => $value) {
            $normalizedKey = strtolower((string) $key);
            if (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 '';
    }
}
