<?php

declare(strict_types=1);

namespace ShieldX\Core;

defined('ABSPATH') || exit;

final class NotificationManager
{
    public const NOTIFICATIONS_ADDON_ID = 'shieldx-notifications';
    public const DIGEST_CRON_HOOK = 'shieldx_notification_digest_send';
    public const DIGEST_QUEUE_OPTION = 'shieldx_notification_digest_queue';
    private const DEDUPE_SECONDS = 900;
    private const DIGEST_MAX_ITEMS = 200;
    private const DIGEST_DETAIL_LIMIT = 50;

    /** @var NotificationSettings|null */
    private $settings;

    public function __construct(?NotificationSettings $settings = null)
    {
        $this->settings = $settings;
    }

    public static function fromWordPress(): self
    {
        return new self();
    }

    public static function notificationsAddonActive(): bool
    {
        return isset(AddonRegistry::fromWordPress()->manifests()[self::NOTIFICATIONS_ADDON_ID]);
    }

    public function register(): void
    {
        if (!function_exists('add_action')) {
            return;
        }

        add_action('shieldx_audit_event', [$this, 'handleAuditEvent'], 10, 4);
        add_action('shieldx_settings_imported', [$this, 'handleSettingsImported'], 10, 1);
        add_action(self::DIGEST_CRON_HOOK, [$this, 'handleDigestCron']);
    }

    /**
     * @param array<string,mixed> $context
     * @param array<string,mixed> $record
     */
    public function handleAuditEvent(string $severity, string $event, array $context, array $record): void
    {
        if (!self::notificationsAddonActive()) {
            return;
        }

        $eventKeys = $this->eventKeysFromAudit($event, $context);
        if (empty($eventKeys)) {
            return;
        }

        $payload = array_merge($context, [
            'audit_event' => $event,
            'audit_severity' => $severity,
            'audit_time' => (string) ($record['time'] ?? gmdate('c')),
        ]);
        foreach ($eventKeys as $eventKey) {
            $this->notify($eventKey, $payload, $severity);
        }
    }

    /**
     * @param array<int,string> $modules
     */
    public function handleSettingsImported(array $modules): void
    {
        if (!self::notificationsAddonActive()) {
            return;
        }

        $this->notify('system.settings_changed', [
            'operation' => 'settings_imported',
            'modules' => $modules,
        ], 'info');
    }

    /**
     * @param array<string,mixed> $context
     */
    public function notify(string $eventKey, array $context = [], string $severity = 'info'): bool
    {
        if (!self::notificationsAddonActive()) {
            return false;
        }

        $catalog = NotificationSettings::eventCatalog();
        if (!isset($catalog[$eventKey]) || !$this->settings()->isEnabled($eventKey)) {
            return false;
        }

        $event = $catalog[$eventKey];
        if (empty($event['critical']) && !empty($event['noisy']) && $this->isThrottled($eventKey, $context)) {
            return false;
        }

        if (!$this->shouldSendImmediately($event, $severity)) {
            if (!$this->settings()->digestEnabled()) {
                return false;
            }

            return $this->queueDigest($eventKey, $event, $context, $severity);
        }

        $recipients = $this->recipients();
        if (empty($recipients) || !function_exists('wp_mail')) {
            return false;
        }

        $subject = $this->subject($eventKey, (string) $event['label'], $severity);
        $message = EmailTemplate::render(
            $subject,
            $this->messageHtml($eventKey, $event, $context, $severity),
            (string) $event['description']
        );

        try {
            return (bool) wp_mail($recipients, $subject, $message, EmailTemplate::headers());
        } catch (\Throwable $error) {
            \ShieldX\Support\ErrorLog::write('SHIELD-X notification mail failed: ' . $error->getMessage());

            return false;
        }
    }

    public static function allowsImmediateMail(bool $critical, ?NotificationSettings $settings = null): bool
    {
        $settings = $settings ?? NotificationSettings::fromWordPress();
        $mode = $settings->deliveryMode();

        if ($mode === NotificationSettings::DELIVERY_IMMEDIATE) {
            return true;
        }

        if (in_array($mode, [NotificationSettings::DELIVERY_CRITICAL_ONLY, NotificationSettings::DELIVERY_DIGEST_CRITICAL_IMMEDIATE], true)) {
            return $critical;
        }

        return false;
    }

    public function sendDigest(): bool
    {
        if (!self::notificationsAddonActive() || !$this->settings()->digestEnabled()) {
            return false;
        }

        $queue = $this->digestQueue();
        $items = isset($queue['items']) && is_array($queue['items']) ? $queue['items'] : [];
        if (empty($items)) {
            return false;
        }

        $recipients = $this->recipients();
        if (empty($recipients) || !function_exists('wp_mail')) {
            return false;
        }

        $count = count($items);
        $subject = $this->digestSubject($count);
        $message = EmailTemplate::render(
            $subject,
            $this->digestHtml($queue),
            sprintf(
                /* translators: %d: number of queued SHIELD-X notification events in the digest. */
                _n('%d accumulated SHIELD-X notification event.', '%d accumulated SHIELD-X notification events.', $count, 'xerotact-shield-x-security'),
                $count
            )
        );

        try {
            $sent = (bool) wp_mail($recipients, $subject, $message, EmailTemplate::headers());
        } catch (\Throwable $error) {
            \ShieldX\Support\ErrorLog::write('SHIELD-X notification digest mail failed: ' . $error->getMessage());
            $sent = false;
        }

        if ($sent) {
            $this->clearDigestQueue();
        }

        return $sent;
    }

    public function handleDigestCron(): void
    {
        $this->sendDigest();
    }

    public function clearDigestQueue(): void
    {
        if (function_exists('update_option')) {
            try {
                update_option(self::DIGEST_QUEUE_OPTION, $this->emptyDigestQueue(), false);
            } catch (\Throwable $error) {
                \ShieldX\Support\ErrorLog::write('SHIELD-X notification digest queue clear failed: ' . $error->getMessage());
            }
        }
    }

    /**
     * @param array<string,mixed> $context
     * @return array<int,string>
     */
    private function eventKeysFromAudit(string $event, array $context): array
    {
        if ($event === 'scan.scheduled_completed') {
            if ((int) ($context['issues_found'] ?? 0) <= 0) {
                return [];
            }

            return [$this->scannerEventKey($context)];
        }

        $eventKey = $this->eventKeyFromAudit($event, $context);

        return $eventKey !== '' ? [$eventKey] : [];
    }

    /**
     * @param array<string,mixed> $context
     */
    private function eventKeyFromAudit(string $event, array $context): string
    {
        $reason = (string) ($context['reason'] ?? '');
        $catalog = NotificationSettings::eventCatalog();

        if (isset($catalog[$event])) {
            return $event;
        }

        if ($event === 'waf.block') {
            if (strpos($reason, 'reputation_') === 0) {
                return 'firewall.reputation_match';
            }

            return 'firewall.waf_block';
        }

        if ($event === 'waf.match') {
            $action = (string) ($context['action'] ?? '');
            if ($action === 'challenge') {
                return 'firewall.waf_challenge';
            }

            return (int) ($context['score'] ?? 0) >= 45
                ? 'firewall.waf_high_confidence_hit'
                : 'firewall.waf_match';
        }

        if ($event === 'login.lockout_created' || $event === 'login.lockout_block') {
            return 'login.lockout';
        }

        if ($event === 'login.failure') {
            return 'login.failure_burst';
        }

        if (in_array($event, ['login.2fa_disabled', 'login.two_factor_disabled', 'login_security.2fa_disabled'], true)) {
            return 'login.2fa_disabled';
        }

        if (in_array($event, [
            'login.2fa_reset',
            'login.two_factor_reset_requested',
            'login.two_factor_reset_completed',
            'login_security.2fa_reset_code_sent',
            'login_security.2fa_reset_code_failed',
            'login_security.2fa_reset_code_confirmed',
            'login_security.recovery_codes_regenerated',
        ], true)) {
            return 'login.2fa_reset';
        }

        if ($event === 'emergency_recovery.completed' || $event === 'login_security.owner_recovery_used') {
            return 'login.recovery_used';
        }

        if ($event === 'scan.scheduled_failed') {
            return 'scanner.scan_failed';
        }

        if ($event === 'scan.schedule_skipped') {
            return '';
        }

        if (strpos($event, 'scanner.') === 0 || strpos($event, 'scan.') === 0) {
            return $this->scannerEventKey($context);
        }

        if ($event === 'lockdown.policy_saved') {
            return 'hardening.lockdown_policy_changed';
        }

        if ($event === 'lockdown.reverted') {
            return 'hardening.lockdown_reverted';
        }

        if ($event === 'lockdown.applied' || $event === 'lockdown.reapplied') {
            return empty($context['ok']) ? 'hardening.lockdown_failure' : 'hardening.lockdown_applied';
        }

        if ($event === 'hardening.saved' || $event === 'hardening.applied') {
            return 'hardening.hardening_applied';
        }

        if (strpos($event, 'update.') === 0) {
            return 'system.update_coordination';
        }

        if (strpos($event, 'log_rotation.') === 0) {
            return 'system.log_rotation';
        }

        if ($event === 'restore_point.pre_change_failed') {
            return 'backup.restore_failed';
        }

        if (strpos($event, 'restore_point.') === 0 || strpos($event, 'backup.') === 0 || strpos($event, 'shadow.') === 0) {
            return $this->backupEventKey($event, $context);
        }

        if ($event === 'human_challenge.settings_saved') {
            return 'system.human_challenge_changed';
        }

        return '';
    }

    /**
     * @param array<string,mixed> $context
     */
    private function scannerEventKey(array $context): string
    {
        $type = strtolower((string) ($context['scan_type'] ?? $context['type'] ?? $context['scanner'] ?? $context['scope'] ?? ''));
        $severity = strtolower((string) ($context['severity'] ?? ''));
        $issuesFound = (int) ($context['issues_found'] ?? 0);

        if ($type === 'database' || strpos((string) ($context['path'] ?? ''), 'db:') === 0) {
            return 'scanner.db_finding';
        }

        if ($type === 'vulnerability' || strpos((string) ($context['path'] ?? ''), 'software:') === 0) {
            return 'scanner.vulnerability_finding';
        }

        if (in_array($severity, ['critical', 'high'], true)) {
            return 'scanner.high_finding';
        }

        if ($severity === 'medium') {
            return 'scanner.medium_finding';
        }

        if ($issuesFound > 0) {
            return 'scanner.high_finding';
        }

        return 'scanner.scan_completed';
    }

    /**
     * @param array<string,mixed> $context
     */
    private function backupEventKey(string $event, array $context): string
    {
        if (strpos($event, 'schedule_skipped') !== false) {
            return '';
        }

        if (strpos($event, 'shadow.') === 0 || !empty($context['changed_files'])) {
            return 'backup.shadow_changes';
        }

        $failed = strpos($event, 'failed') !== false
            || (array_key_exists('ok', $context) && empty($context['ok']))
            || (string) ($context['status'] ?? '') === 'failed';

        if ($failed) {
            return $this->isRestoreApplicationEvent($event) ? 'backup.restore_failed' : 'backup.backup_failed';
        }

        if (strpos($event, 'backup.') === 0 && strpos($event, 'scheduled_completed') !== false) {
            if (!empty($context['notified']) || empty($context['report_requested'])) {
                return '';
            }

            return 'backup.backup_completed';
        }

        if (strpos($event, 'restore_point.') === 0) {
            if ($this->isRestoreApplicationEvent($event)) {
                return 'backup.restore_restored';
            }

            return strpos($event, 'scheduled_completed') !== false || strpos($event, 'created') !== false
                ? 'backup.restore_point_created'
                : '';
        }

        if ($this->isRestoreApplicationEvent($event)) {
            return 'backup.restore_restored';
        }

        return '';
    }

    private function isRestoreApplicationEvent(string $event): bool
    {
        return strpos($event, 'restored') !== false
            || strpos($event, 'restore_applied') !== false
            || strpos($event, 'restore_completed') !== false
            || strpos($event, 'restore_failed') !== false;
    }

    /**
     * @return array<int,string>
     */
    private function recipients(): array
    {
        $recipients = $this->settings()->recipients();
        if (!empty($recipients)) {
            return $recipients;
        }

        try {
            $adminEmail = function_exists('get_option') ? (string) get_option('admin_email', '') : '';
        } catch (\Throwable $error) {
            \ShieldX\Support\ErrorLog::write('SHIELD-X notification admin email read failed: ' . $error->getMessage());
            $adminEmail = '';
        }
        $email = NotificationSettings::sanitizeEmails([$adminEmail]);

        return $email;
    }

    private function settings(): NotificationSettings
    {
        if (!$this->settings instanceof NotificationSettings) {
            $this->settings = NotificationSettings::fromWordPress();
        }

        return $this->settings;
    }

    private function subject(string $eventKey, string $label, string $severity): string
    {
        $site = '';
        if (function_exists('wp_parse_url') && function_exists('home_url')) {
            try {
                $site = (string) wp_parse_url(home_url('/'), PHP_URL_HOST);
            } catch (\Throwable $error) {
                \ShieldX\Support\ErrorLog::write('SHIELD-X notification site host read failed: ' . $error->getMessage());
            }
        }
        $prefix = $site !== '' ? '[SHIELD-X ' . $site . '] ' : '[SHIELD-X] ';

        return $prefix . $label . ' (' . $eventKey . ')';
    }

    /**
     * @param array{label:string,group:string,description:string,noisy:bool,critical:bool} $event
     * @param array<string,mixed> $context
     */
    private function messageHtml(string $eventKey, array $event, array $context, string $severity): string
    {
        $redacted = $this->redactContext($context);
        $displaySeverity = $this->displaySeverity($event, $severity);
        $tone = $this->severityTone($displaySeverity);
        $time = gmdate('c');
        $summary = [
            __('Event', 'xerotact-shield-x-security') => (string) $event['label'],
            __('Severity', 'xerotact-shield-x-security') => $tone['label'],
            __('Group', 'xerotact-shield-x-security') => (string) $event['group'],
            __('Event key', 'xerotact-shield-x-security') => $eventKey,
            __('Time', 'xerotact-shield-x-security') => $time,
        ];
        $important = $this->importantContext($redacted);
        $nextStep = $this->recommendedStep((string) $event['group'], $eventKey);

        $html = '<div style="margin:0 0 18px;padding:16px 18px;background:' . $tone['background'] . ';border:1px solid ' . $tone['border'] . ';border-left:5px solid ' . $tone['accent'] . ';border-radius:8px;">'
            . '<p style="margin:0 0 10px;color:#101935;font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;">'
            . '<span style="display:inline-block;margin:0 8px 6px 0;padding:5px 9px;background:' . $tone['accent'] . ';border-radius:999px;color:#ffffff;font-size:12px;line-height:1;">' . EmailTemplate::escape($tone['label']) . '</span>'
            . '<span style="color:#435579;">' . EmailTemplate::escape((string) $event['group']) . '</span>'
            . '</p>'
            . '<p style="margin:0;color:#101935;font-size:16px;line-height:1.5;font-weight:700;">' . EmailTemplate::escape((string) $event['description']) . '</p>'
            . '</div>';

        $html .= EmailTemplate::details($summary);

        if (!empty($important)) {
            $html .= $this->sectionLabel(__('Key details', 'xerotact-shield-x-security'));
            $html .= EmailTemplate::details($important);
        }

        if ($nextStep !== '') {
            $html .= '<div style="margin:18px 0;padding:14px 16px;background:#f8fafc;border:1px solid #dce6f5;border-radius:8px;color:#334155;font-size:14px;line-height:1.55;">'
                . '<strong style="display:block;margin:0 0 4px;color:#101935;font-size:14px;">' . EmailTemplate::escape(__('Suggested next step', 'xerotact-shield-x-security')) . '</strong>'
                . EmailTemplate::escape($nextStep)
                . '</div>';
        }

        $html .= $this->sectionLabel(__('Full redacted context', 'xerotact-shield-x-security'));
        $html .= EmailTemplate::preformatted($this->diagnosticBody($eventKey, $event, $redacted, $displaySeverity));

        return $html;
    }

    /**
     * @param array{label:string,group:string,description:string,noisy:bool,critical:bool} $event
     * @param array<string,mixed> $context
     */
    private function diagnosticBody(string $eventKey, array $event, array $context, string $severity): string
    {
        $lines = [
            'SHIELD-X notification',
            '',
            'Event: ' . (string) $event['label'],
            'Key: ' . $eventKey,
            'Group: ' . (string) $event['group'],
            'Severity: ' . $severity,
            'Time: ' . gmdate('c'),
            '',
            (string) $event['description'],
            '',
            'Context:',
        ];

        foreach ($context as $key => $value) {
            $lines[] = '- ' . $key . ': ' . $this->stringify($value);
        }

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

    /**
     * @param array{label:string,group:string,description:string,noisy:bool,critical:bool} $event
     */
    private function displaySeverity(array $event, string $severity): string
    {
        $severity = strtolower(trim($severity));
        if (empty($event['critical']) && in_array($severity, ['critical', 'error', 'high'], true)) {
            return 'warning';
        }

        return $severity !== '' ? $severity : 'info';
    }

    /**
     * @return array{label:string,accent:string,border:string,background:string}
     */
    private function severityTone(string $severity): array
    {
        $severity = strtolower(trim($severity));
        if (in_array($severity, ['critical', 'error', 'high'], true)) {
            return [
                'label' => __('Critical', 'xerotact-shield-x-security'),
                'accent' => '#dc2626',
                'border' => '#fecaca',
                'background' => '#fef2f2',
            ];
        }

        if (in_array($severity, ['warning', 'medium'], true)) {
            return [
                'label' => __('Warning', 'xerotact-shield-x-security'),
                'accent' => '#d97706',
                'border' => '#fed7aa',
                'background' => '#fff7ed',
            ];
        }

        return [
            'label' => __('Info', 'xerotact-shield-x-security'),
            'accent' => '#2563eb',
            'border' => '#bfdbfe',
            'background' => '#eff6ff',
        ];
    }

    private function sectionLabel(string $label): string
    {
        return '<p style="margin:20px 0 8px;color:#101935;font-size:14px;font-weight:700;">' . EmailTemplate::escape($label) . '</p>';
    }

    /**
     * @param array<string,mixed> $context
     * @return array<string,string>
     */
    private function importantContext(array $context): array
    {
        $labels = [
            'action' => __('Action', 'xerotact-shield-x-security'),
            'mode' => __('Mode', 'xerotact-shield-x-security'),
            'reason' => __('Reason', 'xerotact-shield-x-security'),
            'ip' => __('IP address', 'xerotact-shield-x-security'),
            'method' => __('Method', 'xerotact-shield-x-security'),
            'uri' => __('URL/path', 'xerotact-shield-x-security'),
            'path' => __('Path', 'xerotact-shield-x-security'),
            'rule_id' => __('Rule', 'xerotact-shield-x-security'),
            'score' => __('Score', 'xerotact-shield-x-security'),
            'operation' => __('Operation', 'xerotact-shield-x-security'),
            'status' => __('Status', 'xerotact-shield-x-security'),
            'scan_type' => __('Scan type', 'xerotact-shield-x-security'),
            'issues_found' => __('Issues found', 'xerotact-shield-x-security'),
            'user_id' => __('User ID', 'xerotact-shield-x-security'),
            'restore_id' => __('Restore point', 'xerotact-shield-x-security'),
            'candidate_id' => __('Recovery candidate', 'xerotact-shield-x-security'),
            'type' => __('Type', 'xerotact-shield-x-security'),
            'hour' => __('Hour', 'xerotact-shield-x-security'),
            'attempted' => __('Attempted', 'xerotact-shield-x-security'),
            'failed' => __('Failed', 'xerotact-shield-x-security'),
            'failure_rate' => __('Failure rate', 'xerotact-shield-x-security'),
        ];

        $details = [];
        foreach ($labels as $key => $label) {
            if (!array_key_exists($key, $context)) {
                continue;
            }

            $details[$label] = $this->compactString($context[$key]);
            if (count($details) >= 8) {
                break;
            }
        }

        return $details;
    }

    /**
     * @param mixed $value
     */
    private function compactString($value): string
    {
        $text = $this->stringify($value);
        $text = preg_replace('/\s+/', ' ', $text) ?: $text;
        if (strlen($text) > 180) {
            return substr($text, 0, 180) . '... [truncated]';
        }

        return $text;
    }

    private function recommendedStep(string $group, string $eventKey = ''): string
    {
        if ($group === 'Firewall') {
            return __('Review the source, URL, and matched rule. Allowlist only if the request is expected and trusted.', 'xerotact-shield-x-security');
        }

        if ($group === 'Login Security') {
            return __('Review the affected user or IP before changing lockout, 2FA, or recovery settings.', 'xerotact-shield-x-security');
        }

        if ($group === 'Scanners') {
            if ($eventKey === 'scanner.scan_failed') {
                return __('Open SHIELD-X → Malware/Virus, check the scan status and error, fix the cause, then rerun the scan.', 'xerotact-shield-x-security');
            }

            return __('Open the scanner results and confirm whether the finding needs cleanup, quarantine, or review.', 'xerotact-shield-x-security');
        }

        if ($group === 'Backup/Restore') {
            return __('Check the restore point or backup status before making further file or database changes.', 'xerotact-shield-x-security');
        }

        if ($group === 'Hardening/Lockdown') {
            return __('Review the hardening or lockdown status and confirm the site still behaves normally.', 'xerotact-shield-x-security');
        }

        if ($group === 'SHIELD-X Pulse') {
            return __('Open Pulse, confirm whether the anomaly is still active, then retry the monitor after fixing the cause.', 'xerotact-shield-x-security');
        }

        if ($group === 'SHIELD-X Recovery') {
            return __('Review the Recovery candidate carefully. Restore only after confirming the selected restore point.', 'xerotact-shield-x-security');
        }

        return __('Open SHIELD-X and review the related module before taking action.', 'xerotact-shield-x-security');
    }

    /**
     * @param array{label:string,group:string,description:string,noisy:bool,critical:bool} $event
     */
    private function shouldSendImmediately(array $event, string $severity): bool
    {
        unset($severity);

        return self::allowsImmediateMail(!empty($event['critical']), $this->settings());
    }

    /**
     * @param array{label:string,group:string,description:string,noisy:bool,critical:bool} $event
     * @param array<string,mixed> $context
     */
    private function queueDigest(string $eventKey, array $event, array $context, string $severity): bool
    {
        if (!function_exists('get_option') || !function_exists('update_option')) {
            return false;
        }

        $queue = $this->digestQueue();
        $items = isset($queue['items']) && is_array($queue['items']) ? $queue['items'] : [];
        $items[] = [
            'event_key' => $eventKey,
            'label' => (string) $event['label'],
            'group' => (string) $event['group'],
            'description' => (string) $event['description'],
            'severity' => $this->displaySeverity($event, $severity),
            'time' => gmdate('c'),
            'context' => $this->digestContext($this->redactContext($context)),
        ];

        $overflow = max(0, (int) ($queue['overflow'] ?? 0));
        if (count($items) > self::DIGEST_MAX_ITEMS) {
            $overflow += count($items) - self::DIGEST_MAX_ITEMS;
            $items = array_slice($items, -self::DIGEST_MAX_ITEMS);
        }

        $now = gmdate('c');
        $createdAt = is_scalar($queue['created_at'] ?? null) && (string) $queue['created_at'] !== ''
            ? (string) $queue['created_at']
            : $now;

        try {
            return (bool) update_option(self::DIGEST_QUEUE_OPTION, [
                'created_at' => $createdAt,
                'updated_at' => $now,
                'overflow' => $overflow,
                'items' => $items,
            ], false);
        } catch (\Throwable $error) {
            \ShieldX\Support\ErrorLog::write('SHIELD-X notification digest queue write failed: ' . $error->getMessage());

            return false;
        }
    }

    /**
     * @return array{created_at:string,updated_at:string,overflow:int,items:array<int,array<string,mixed>>}
     */
    private function digestQueue(): array
    {
        try {
            $stored = function_exists('get_option') ? get_option(self::DIGEST_QUEUE_OPTION, []) : [];
        } catch (\Throwable $error) {
            \ShieldX\Support\ErrorLog::write('SHIELD-X notification digest queue read failed: ' . $error->getMessage());

            return $this->emptyDigestQueue();
        }
        if (!is_array($stored)) {
            return $this->emptyDigestQueue();
        }

        $items = isset($stored['items']) && is_array($stored['items']) ? $stored['items'] : [];

        return [
            'created_at' => is_scalar($stored['created_at'] ?? null) ? (string) $stored['created_at'] : '',
            'updated_at' => is_scalar($stored['updated_at'] ?? null) ? (string) $stored['updated_at'] : '',
            'overflow' => max(0, (int) ($stored['overflow'] ?? 0)),
            'items' => array_values(array_filter($items, 'is_array')),
        ];
    }

    /**
     * @return array{created_at:string,updated_at:string,overflow:int,items:array<int,array<string,mixed>>}
     */
    private function emptyDigestQueue(): array
    {
        return [
            'created_at' => '',
            'updated_at' => '',
            'overflow' => 0,
            'items' => [],
        ];
    }

    private function digestSubject(int $count): string
    {
        $site = function_exists('wp_parse_url') && function_exists('home_url')
            ? (string) wp_parse_url(home_url('/'), PHP_URL_HOST)
            : '';
        $prefix = $site !== '' ? '[SHIELD-X ' . $site . '] ' : '[SHIELD-X] ';

        return $prefix . sprintf(
            /* translators: %d: number of queued SHIELD-X notification events in the digest. */
            _n('Notification digest (%d event)', 'Notification digest (%d events)', $count, 'xerotact-shield-x-security'),
            $count
        );
    }

    /**
     * @param array{created_at:string,updated_at:string,overflow:int,items:array<int,array<string,mixed>>} $queue
     */
    private function digestHtml(array $queue): string
    {
        $items = $queue['items'];
        $count = count($items);
        $overflow = (int) $queue['overflow'];
        $summary = [
            __('Events in report', 'xerotact-shield-x-security') => (string) $count,
            __('First queued', 'xerotact-shield-x-security') => $queue['created_at'] !== '' ? $queue['created_at'] : __('Unknown', 'xerotact-shield-x-security'),
            __('Last queued', 'xerotact-shield-x-security') => $queue['updated_at'] !== '' ? $queue['updated_at'] : __('Unknown', 'xerotact-shield-x-security'),
            __('Generated', 'xerotact-shield-x-security') => gmdate('c'),
        ];
        if ($overflow > 0) {
            $summary[__('Older queued events trimmed', 'xerotact-shield-x-security')] = (string) $overflow;
        }

        $html = EmailTemplate::notice(
            __('Accumulated notification report', 'xerotact-shield-x-security'),
            sprintf(
                /* translators: %d: number of queued SHIELD-X notification events in the digest. */
                _n('SHIELD-X accumulated %d event into this report.', 'SHIELD-X accumulated %d events into this report.', $count, 'xerotact-shield-x-security'),
                $count
            ),
            'info'
        );
        $html .= EmailTemplate::details($summary);

        $totals = [];
        foreach ($items as $item) {
            $key = (string) ($item['event_key'] ?? '');
            $label = (string) ($item['label'] ?? $key);
            $group = (string) ($item['group'] ?? '');
            $bucket = trim($group . ' / ' . $label . ($key !== '' ? ' (' . $key . ')' : ''));
            $totals[$bucket] = (int) ($totals[$bucket] ?? 0) + 1;
        }
        arsort($totals);

        $eventTotals = [];
        foreach ($totals as $label => $total) {
            $eventTotals[$label] = (string) $total;
            if (count($eventTotals) >= 20) {
                break;
            }
        }

        $html .= $this->sectionLabel(__('Event totals', 'xerotact-shield-x-security'));
        $html .= EmailTemplate::details($eventTotals);
        $html .= $this->sectionLabel(__('Recent event details', 'xerotact-shield-x-security'));
        $html .= EmailTemplate::preformatted($this->digestDiagnosticBody($items));

        return $html;
    }

    /**
     * @param array<int,array<string,mixed>> $items
     */
    private function digestDiagnosticBody(array $items): string
    {
        $count = count($items);
        $visible = array_slice($items, -self::DIGEST_DETAIL_LIMIT);
        $lines = [
            'SHIELD-X notification digest',
            '',
            sprintf('Showing latest %d of %d queued events.', count($visible), $count),
            '',
        ];

        foreach ($visible as $index => $item) {
            $lines[] = '#' . (string) ($index + 1) . ' [' . (string) ($item['severity'] ?? 'info') . '] '
                . (string) ($item['time'] ?? '') . ' '
                . (string) ($item['group'] ?? '') . ' / '
                . (string) ($item['label'] ?? '')
                . ' (' . (string) ($item['event_key'] ?? '') . ')';
            $context = isset($item['context']) && is_array($item['context']) ? $item['context'] : [];
            foreach ($context as $key => $value) {
                $lines[] = '- ' . (string) $key . ': ' . $this->stringify($value);
            }
            $lines[] = '';
        }

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

    /**
     * @param array<string,mixed> $context
     * @return array<string,string>
     */
    private function digestContext(array $context): array
    {
        $important = [];
        foreach ($context as $key => $value) {
            $important[(string) $key] = $this->compactString($value);
            if (count($important) >= 12) {
                break;
            }
        }

        return $important;
    }

    /**
     * @param array<string,mixed> $context
     */
    private function isThrottled(string $eventKey, array $context): bool
    {
        if (!function_exists('get_transient') || !function_exists('set_transient')) {
            return false;
        }

        $key = 'shieldx_notify_' . md5($eventKey . '|' . $this->fingerprint($context));
        try {
            if (get_transient($key)) {
                return true;
            }

            set_transient($key, '1', self::DEDUPE_SECONDS);
        } catch (\Throwable $error) {
            \ShieldX\Support\ErrorLog::write('SHIELD-X notification throttle failed: ' . $error->getMessage());

            return false;
        }

        return false;
    }

    /**
     * @param array<string,mixed> $context
     */
    private function fingerprint(array $context): string
    {
        $parts = [
            (string) ($context['ip'] ?? ''),
            (string) ($context['uri'] ?? $context['path'] ?? ''),
            (string) ($context['rule_id'] ?? ''),
            is_array($context['rules'] ?? null) ? implode(',', array_slice(array_map('strval', (array) $context['rules']), 0, 3)) : '',
            (string) ($context['action'] ?? ''),
        ];

        $fingerprint = implode('|', $parts);
        if (trim(str_replace('|', '', $fingerprint)) !== '') {
            return $fingerprint;
        }

        $fallbackKeys = [
            'operation',
            'status',
            'manual',
            'scan_type',
            'type',
            'scan_id',
            'issues_found',
            'missing',
            'warning',
            'module_missing',
            'module_warning',
            'backup_id',
            'restore_id',
            'deleted_files',
            'trimmed_files',
            'deleted_rows',
            'failed_files',
            'failed_directories',
            'truncated',
        ];
        $fallback = [];
        foreach ($fallbackKeys as $key) {
            if (!array_key_exists($key, $context)) {
                continue;
            }

            $value = $context[$key];
            if (is_bool($value)) {
                $fallback[] = $key . '=' . ($value ? '1' : '0');
                continue;
            }

            if (is_scalar($value)) {
                $fallback[] = $key . '=' . (string) $value;
            }
        }

        return implode('|', $fallback);
    }

    /**
     * @param array<string,mixed> $context
     * @return array<string,mixed>
     */
    private function redactContext(array $context): array
    {
        $redacted = [];
        foreach ($context as $key => $value) {
            $redacted[$key] = $this->redactValue((string) $key, $value);
        }

        return $redacted;
    }

    /**
     * @param mixed $value
     * @return mixed
     */
    private function redactValue(string $key, $value)
    {
        $lower = strtolower($key);
        foreach (['token', 'secret', 'password', 'auth', 'salt', 'cookie', 'nonce', 'recovery', 'body'] as $needle) {
            if (strpos($lower, $needle) !== false) {
                return '[redacted]';
            }
        }

        if (is_array($value)) {
            $output = [];
            foreach ($value as $childKey => $childValue) {
                $output[(string) $childKey] = $this->redactValue((string) $childKey, $childValue);
            }

            return $output;
        }

        if (is_scalar($value)) {
            $text = (string) $value;
            if (strlen($text) > 500) {
                return substr($text, 0, 500) . '... [truncated]';
            }

            return $text;
        }

        return '[unserializable]';
    }

    /**
     * @param mixed $value
     */
    private function stringify($value): string
    {
        if (is_array($value)) {
            $json = wp_json_encode($value);

            return is_string($json) ? $json : '[array]';
        }

        return (string) $value;
    }
}
