<?php

declare(strict_types=1);

namespace ShieldX\Scanner;

defined('ABSPATH') || exit;

use ShieldX\Core\EmailTemplate;
use ShieldX\Core\NotificationManager;
use ShieldX\Core\SchedulerBridge;
use ShieldX\Security\AuditLogger;

final class ScheduledScanRunner
{
    public const HOOK = 'shieldx_scheduled_scan';
    private const LOCK_OPTION_PREFIX = 'shieldx_scheduled_scan_lease_';
    private const LOCK_TTL = 1800;
    private const PROGRESS_OPTION_PREFIX = 'shieldx_scheduled_scan_progress_';
    private const PROGRESS_CONTINUATION_DELAY = 60;
    private const PROGRESS_MAX_BATCHES_PER_RUN = 3;
    private const PROGRESS_MAX_SECONDS_PER_RUN = 24;

    /** @var LocalScanner */
    private $scanner;

    /** @var ScheduledScanSettings */
    private $settings;

    /** @var AuditLogger|null */
    private $logger;

    /** @var array<string,string> */
    private $lockTokens = [];

    public function __construct(LocalScanner $scanner, ScheduledScanSettings $settings, ?AuditLogger $logger = null)
    {
        $this->scanner = $scanner;
        $this->settings = $settings;
        $this->logger = $logger;
    }

    /**
     * @param array<string,array<string,mixed>> $schedules
     * @return array<string,array<string,mixed>>
     */
    public static function cronSchedules(array $schedules): array
    {
        if (!isset($schedules['weekly'])) {
            $schedules['weekly'] = [
                'interval' => 7 * 24 * 60 * 60,
                'display' => function_exists('__') ? __('Once Weekly', 'xerotact-shield-x-security') : 'Once Weekly',
            ];
        }

        return $schedules;
    }

    public static function ensureScheduled(ScheduledScanSettings $settings, bool $force = false): void
    {
        if (!function_exists('wp_next_scheduled') || !function_exists('wp_schedule_event') || !function_exists('wp_clear_scheduled_hook')) {
            return;
        }

        if (!SchedulerBridge::active()) {
            wp_clear_scheduled_hook(self::HOOK, []);
            foreach (ScheduledScanSettings::types() as $type) {
                wp_clear_scheduled_hook(self::HOOK, [$type]);
            }
            return;
        }

        wp_clear_scheduled_hook(self::HOOK, []);

        foreach (ScheduledScanSettings::types() as $type) {
            $args = [$type];

            if (!$settings->enabled($type)) {
                wp_clear_scheduled_hook(self::HOOK, $args);
                continue;
            }

            $currentSchedule = function_exists('wp_get_schedule') ? wp_get_schedule(self::HOOK, $args) : '';
            if ($force || ($currentSchedule !== '' && $currentSchedule !== false && $currentSchedule !== $settings->recurrence($type))) {
                wp_clear_scheduled_hook(self::HOOK, $args);
            }

            if (!wp_next_scheduled(self::HOOK, $args)) {
                wp_schedule_event(self::nextRunTimestamp($settings->recurrence($type)), $settings->recurrence($type), self::HOOK, $args);
            }
        }
    }

    public static function nextRunTimestamp(string $recurrence, ?int $now = null): int
    {
        $now = $now ?? time();

        return $now + self::intervalForRecurrence($recurrence);
    }

    public static function clearScheduled(): void
    {
        if (function_exists('wp_clear_scheduled_hook')) {
            wp_clear_scheduled_hook(self::HOOK, []);
            foreach (ScheduledScanSettings::types() as $type) {
                wp_clear_scheduled_hook(self::HOOK, [$type]);
            }
        }

        self::releaseLock();
    }

    /**
     * @return array<string,mixed>
     */
    public static function scheduleState(): array
    {
        $sections = [];
        $firstNext = 0;
        $firstSchedule = '';

        foreach (ScheduledScanSettings::types() as $type) {
            $args = [$type];
            $next = function_exists('wp_next_scheduled') ? (int) wp_next_scheduled(self::HOOK, $args) : 0;
            $schedule = function_exists('wp_get_schedule') ? (string) wp_get_schedule(self::HOOK, $args) : '';
            $sections[$type] = [
                'next_run' => $next > 0 ? self::formatTimestamp($next) : '',
                'next_run_timestamp' => $next,
                'wp_schedule' => $schedule,
                'locked' => self::isLocked($type),
            ];

            if ($next > 0 && ($firstNext === 0 || $next < $firstNext)) {
                $firstNext = $next;
                $firstSchedule = $schedule;
            }
        }

        return [
            'next_run' => $firstNext > 0 ? self::formatTimestamp($firstNext) : '',
            'next_run_timestamp' => $firstNext,
            'wp_schedule' => $firstSchedule,
            'locked' => self::isLocked(),
            'sections' => $sections,
        ];
    }

    public static function isLocked(string $type = ''): bool
    {
        $types = $type !== '' ? [ScheduledScanSettings::sanitizeType($type)] : ScheduledScanSettings::types();
        foreach ($types as $candidate) {
            if (self::hasActiveOptionLock($candidate)) {
                return true;
            }
        }

        return false;
    }

    /**
     * @return array<string,mixed>
     */
    public function run(bool $force = false, string $type = ScheduledScanSettings::TYPE_FILE): array
    {
        $type = ScheduledScanSettings::sanitizeType($type);
        $lockAcquired = false;

        try {
            if (!$force && !$this->settings->enabled($type)) {
                return [
                    'status' => 'skipped',
                    'reason' => 'schedule_disabled',
                    'scan_type' => $type,
                ];
            }

            if (!$this->acquireLock($type)) {
                $this->safeLog('warning', 'scan.schedule_skipped', ['reason' => 'lock_active', 'scan_type' => $type]);

                $result = [
                    'status' => 'skipped',
                    'reason' => 'lock_active',
                    'error' => 'Another scheduled scan is already running.',
                    'scan_type' => $type,
                ];
                $this->safeRecordRun($result, false, $type);

                return $result;
            }
            $lockAcquired = true;

            $result = $this->runScanForType($type);
            $result['scan_type'] = $type;
            if ((string) ($result['status'] ?? '') === 'running') {
                $this->safeRecordRun($result, false, $type);
                $this->safeScheduleContinuation($type);
                $this->safeLog('info', 'scan.scheduled_continuing', [
                    'scan_type' => $type,
                    'scan_id' => (string) ($result['scan_id'] ?? ''),
                    'percent' => (int) ($result['percent'] ?? 0),
                ]);

                return $result;
            }

            $notified = $this->safeSendNotification($result);
            $this->safeRecordRun($result, $notified, $type);
            if (!SchedulerBridge::controlsTask('core.scan.' . $type)) {
                $this->safeEnsureScheduled($type);
            }

            $this->safeLog((int) ($result['issues_found'] ?? 0) > 0 ? 'warning' : 'info', 'scan.scheduled_completed', [
                'scan_type' => $type,
                'scan_id' => (string) ($result['scan_id'] ?? ''),
                'status' => (string) ($result['status'] ?? ''),
                'issues_found' => (int) ($result['issues_found'] ?? 0),
                'notified' => $notified,
            ]);

            return $result;
        } catch (\Throwable $error) {
            $result = [
                'scan_id' => '',
                'status' => 'failed',
                'issues_found' => 0,
                'error' => $error->getMessage(),
                'scan_type' => $type,
            ];
            $notified = $this->safeSendNotification($result);
            $this->safeRecordRun($result, $notified, $type);
            $this->safeLog('warning', 'scan.scheduled_failed', [
                'scan_type' => $type,
                'error' => $error->getMessage(),
                'notified' => $notified,
            ]);

            return $result;
        } finally {
            if ($lockAcquired) {
                $this->safeReleaseOwnedLock($type);
            }
        }
    }

    /**
     * @param array<string,mixed> $result
     */
    private function safeSendNotification(array $result): bool
    {
        try {
            return $this->sendNotification($result);
        } catch (\Throwable $error) {
            $this->safeLog('warning', 'scan.notification_failed', ['error' => $error->getMessage()]);

            return false;
        }
    }

    /**
     * @param array<string,mixed> $result
     */
    private function safeRecordRun(array $result, bool $notified, string $type): void
    {
        try {
            $this->settings->recordRun($result, $notified, $type);
        } catch (\Throwable $error) {
            $this->safeLog('warning', 'scan.record_run_failed', [
                'scan_type' => $type,
                'error' => $error->getMessage(),
            ]);
        }
    }

    private function safeReleaseOwnedLock(string $type): void
    {
        try {
            $this->releaseOwnedLock($type);
        } catch (\Throwable $error) {
            \ShieldX\Support\ErrorLog::write('SHIELD-X scheduled scan lock release failed: ' . $error->getMessage());
        }
    }

    private function safeScheduleContinuation(string $type): void
    {
        try {
            $this->scheduleContinuation($type);
        } catch (\Throwable $error) {
            $this->safeLog('warning', 'scan.schedule_continuation_failed', [
                'scan_type' => $type,
                'error' => $error->getMessage(),
            ]);
        }
    }

    private function safeEnsureScheduled(string $type): void
    {
        try {
            self::ensureScheduled($this->settings);
        } catch (\Throwable $error) {
            $this->safeLog('warning', 'scan.reschedule_failed', [
                'scan_type' => $type,
                'error' => $error->getMessage(),
            ]);
        }
    }

    /**
     * @param array<string,mixed> $context
     */
    private function safeLog(string $level, string $event, array $context): void
    {
        try {
            $this->log($level, $event, $context);
        } catch (\Throwable $error) {
            \ShieldX\Support\ErrorLog::write(sprintf('SHIELD-X scheduled scan log failed for %s: %s', $event, $error->getMessage()));
        }
    }

    /**
     * @return array<string,mixed>
     */
    private function runScanForType(string $type): array
    {
        if ($type === ScheduledScanSettings::TYPE_DATABASE) {
            if ($this->settings->databaseDepth($type) === 'deep') {
                return $this->runProgressiveFileScan($type, ['database'], 'deep');
            }

            return $this->scanner->runQuickScan(
                $this->settings->maxFiles($type),
                ['database'],
                'scheduled'
            );
        }

        if ($type === ScheduledScanSettings::TYPE_VULNERABILITY) {
            $result = VulnerabilityScanner::fromWordPress()->scan();
            $result['scan_id'] = '';
            $result['scope'] = 'vulnerability';
            $result['scope_label'] = 'Vulnerability scanner';
            $result['files_total'] = 0;
            $result['files_scanned'] = 0;
            $result['files_skipped'] = 0;
            $result['database_items_scanned'] = 0;

            return $result;
        }

        $scopes = $this->settings->scopes($type);

        return $this->runProgressiveFileScan($type, $scopes);
    }

    /**
     * @param array<int,string> $scopes
     * @return array<string,mixed>
     */
    private function runProgressiveFileScan(string $type, array $scopes, string $databaseDepth = 'quick'): array
    {
        $state = $this->readProgressState($type);
        $scanId = (string) ($state['scan_id'] ?? '');
        if ($scanId === '') {
            $job = $this->scanner->startProgressiveScan(
                $this->settings->maxFiles($type),
                $scopes,
                'shield-x-scan',
                true,
                'scan',
                $databaseDepth
            );
            $scanId = (string) ($job['scan_id'] ?? '');
            $this->writeProgressState($type, $scanId);
        }

        $result = $this->runProgressiveBatches($scanId);
        $status = (string) ($result['status'] ?? 'failed');
        if ($status === 'completed') {
            $this->deleteProgressState($type);
            $completed = isset($result['result']) && is_array($result['result']) ? $result['result'] : $result;
            $completed['scheduled_progressive'] = true;
            $completed['scheduled_batches_run'] = (int) ($result['scheduled_batches_run'] ?? 0);

            return $completed;
        }

        if ($status === 'running') {
            $this->writeProgressState($type, $scanId);
            $result['scheduled_progressive'] = true;
            $result['reason'] = 'scheduled_scan_continuing';

            return $result;
        }

        $this->deleteProgressState($type);
        if ($scanId !== '') {
            $job = $this->scanner->startProgressiveScan(
                $this->settings->maxFiles($type),
                $scopes,
                'shield-x-scan',
                true,
                'scan',
                $databaseDepth
            );
            $scanId = (string) ($job['scan_id'] ?? '');
            $this->writeProgressState($type, $scanId);
            $retry = $this->runProgressiveBatches($scanId);
            if ((string) ($retry['status'] ?? '') === 'completed') {
                $this->deleteProgressState($type);
                $completed = isset($retry['result']) && is_array($retry['result']) ? $retry['result'] : $retry;
                $completed['scheduled_progressive'] = true;
                $completed['scheduled_batches_run'] = (int) ($retry['scheduled_batches_run'] ?? 0);

                return $completed;
            }

            $retry['scheduled_progressive'] = true;
            $retry['reason'] = 'scheduled_scan_continuing';

            return $retry;
        }

        return [
            'scan_id' => '',
            'status' => 'failed',
            'error' => (string) ($result['error'] ?? 'Scheduled scan job failed.'),
            'issues_found' => 0,
        ];
    }

    /**
     * @return array<string,mixed>
     */
    private function runProgressiveBatches(string $scanId): array
    {
        $started = microtime(true);
        $result = [
            'scan_id' => $scanId,
            'status' => 'failed',
            'error' => 'Scheduled scan job was not found.',
        ];
        $batches = 0;

        while ($batches < self::PROGRESS_MAX_BATCHES_PER_RUN && (microtime(true) - $started) < self::PROGRESS_MAX_SECONDS_PER_RUN) {
            $result = $this->scanner->runProgressiveScanBatch($scanId);
            $batches++;
            if ((string) ($result['status'] ?? '') !== 'running') {
                break;
            }
        }

        $result['scheduled_batches_run'] = $batches;

        return $result;
    }

    /**
     * @return array<string,mixed>
     */
    private function readProgressState(string $type): array
    {
        if (!function_exists('get_option')) {
            return [];
        }

        $state = get_option($this->progressOptionName($type), []);

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

    private function writeProgressState(string $type, string $scanId): void
    {
        if ($scanId === '' || !function_exists('update_option')) {
            return;
        }

        update_option($this->progressOptionName($type), [
            'scan_id' => $scanId,
            'type' => $type,
            'updated_at' => self::formatTimestamp(time()),
        ], false);
    }

    private function deleteProgressState(string $type): void
    {
        if (function_exists('delete_option')) {
            delete_option($this->progressOptionName($type));
        }
    }

    private function progressOptionName(string $type): string
    {
        return self::PROGRESS_OPTION_PREFIX . ScheduledScanSettings::sanitizeType($type);
    }

    private function scheduleContinuation(string $type): void
    {
        if (!function_exists('wp_schedule_single_event')) {
            return;
        }

        wp_schedule_single_event(time() + self::PROGRESS_CONTINUATION_DELAY, self::HOOK, [$type]);
    }

    private function acquireLock(string $type): bool
    {
        $token = $this->newLockToken();
        $now = time();
        $shieldxLockOption = self::lockOptionName($type);
        $value = (string) $now . ':' . $token;

        // add_option() is backed by a unique option_name index, making this
        // acquisition atomic across overlapping cron requests.
        if (function_exists('add_option') && function_exists('get_option')) {
            if (add_option($shieldxLockOption, $value, '', 'no')) {
                $this->lockTokens[$type] = $token;

                return true;
            }

            self::clearOptionCache($shieldxLockOption);
            $existing = get_option($shieldxLockOption, '');
            $existing = is_scalar($existing) ? (string) $existing : '';
            if (!self::lockValueIsExpired($existing, $now) || !$this->replaceExpiredOptionLock($shieldxLockOption, $existing, $value)) {
                return false;
            }

            $this->lockTokens[$type] = $token;

            return true;
        }

        return true;
    }

    private function releaseOwnedLock(string $type): void
    {
        $token = (string) ($this->lockTokens[$type] ?? '');
        unset($this->lockTokens[$type]);
        if ($token === '') {
            return;
        }

        self::releaseOptionLockOwnedBy($type, $token);
    }

    private static function releaseLock(string $type = ''): void
    {
        $types = $type !== '' ? [ScheduledScanSettings::sanitizeType($type)] : ScheduledScanSettings::types();
        if (function_exists('delete_option')) {
            foreach ($types as $candidate) {
                delete_option(self::lockOptionName($candidate));
            }
        }

    }

    private static function lockOptionName(string $type): string
    {
        return self::LOCK_OPTION_PREFIX . ScheduledScanSettings::sanitizeType($type);
    }

    private static function hasActiveOptionLock(string $type): bool
    {
        if (!function_exists('get_option')) {
            return false;
        }

        $shieldxLockOption = self::lockOptionName($type);
        self::clearOptionCache($shieldxLockOption);
        $value = get_option($shieldxLockOption, '');

        return is_scalar($value) && !self::lockValueIsExpired((string) $value, time());
    }

    private static function lockValueIsExpired(string $value, int $now): bool
    {
        $started = (int) explode(':', $value, 2)[0];

        return $started <= 0 || $started + self::LOCK_TTL < $now;
    }

    private function replaceExpiredOptionLock(string $shieldxLockOption, string $current, string $replacement): bool
    {
        global $wpdb;
        if (!is_object($wpdb) || !isset($wpdb->options) || !method_exists($wpdb, 'update')) {
            return false;
        }

        // Compare the previous value in SQL so two workers that both observe
        // an expired lease cannot take it over at the same time.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Atomic compare-and-swap renewal of an expired scan lease requires the current option_value predicate; cache reads/writes cannot preserve lock ownership.
        $updated = $wpdb->update(
            (string) $wpdb->options,
            ['option_value' => $replacement],
            ['option_name' => $shieldxLockOption, 'option_value' => $current],
            ['%s'],
            ['%s', '%s']
        );

        if ($updated === 1) {
            self::clearOptionCache($shieldxLockOption);
        }

        return $updated === 1;
    }

    private static function releaseOptionLockOwnedBy(string $type, string $token): void
    {
        $shieldxLockOption = self::lockOptionName($type);
        if (!function_exists('get_option')) {
            return;
        }

        self::clearOptionCache($shieldxLockOption);
        $current = get_option($shieldxLockOption, '');
        $current = is_scalar($current) ? (string) $current : '';
        if ($current === '' || substr($current, -strlen($token)) !== $token) {
            return;
        }

        global $wpdb;
        if (is_object($wpdb) && isset($wpdb->options) && method_exists($wpdb, 'delete')) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Atomic owner-verified release of a scan lease requires both option_name and current option_value predicates; caching cannot preserve lock ownership.
            $wpdb->delete((string) $wpdb->options, ['option_name' => $shieldxLockOption, 'option_value' => $current], ['%s', '%s']);
            self::clearOptionCache($shieldxLockOption);

            return;
        }

        if (function_exists('delete_option')) {
            delete_option($shieldxLockOption);
        }
    }

    private static function clearOptionCache(string $shieldxLockOption): void
    {
        if (function_exists('wp_cache_delete')) {
            wp_cache_delete($shieldxLockOption, 'options');
        }
    }

    private function newLockToken(): string
    {
        try {
            return bin2hex(random_bytes(16));
        } catch (\Throwable $error) {
            return hash('sha256', uniqid('shieldx-scan-lock-', true));
        }
    }

    /**
     * @param array<string,mixed> $result
     */
    private function sendNotification(array $result): bool
    {
        if (!NotificationManager::notificationsAddonActive() || !function_exists('wp_mail')) {
            return false;
        }

        $failed = (string) ($result['status'] ?? '') === 'failed';
        $issuesFound = (int) ($result['issues_found'] ?? 0);
        $type = ScheduledScanSettings::sanitizeType((string) ($result['scan_type'] ?? ScheduledScanSettings::TYPE_FILE));
        $critical = $failed || self::hasCriticalScanIssues($result);
        $shouldSend = $this->settings->emailReportsEnabled($type)
            || ($issuesFound > 0 && $this->settings->alertOnIssues($type))
            || ($failed && $this->settings->alertOnFailure($type));

        if (!$critical || !$shouldSend || !NotificationManager::allowsImmediateMail($critical)) {
            return false;
        }

        $recipients = $this->notificationRecipients($type);
        if (empty($recipients)) {
            return false;
        }

        $subject = $this->mailSubject($result);

        return (bool) wp_mail(
            $recipients,
            $subject,
            EmailTemplate::render($subject, $this->mailContent($result), 'SHIELD-X scheduled scan report'),
            EmailTemplate::headers()
        );
    }

    /**
     * @param array<string,mixed> $result
     */
    private static function hasCriticalScanIssues(array $result): bool
    {
        $counts = isset($result['severity_counts']) && is_array($result['severity_counts']) ? $result['severity_counts'] : [];
        foreach (['critical', 'high'] as $severity) {
            if ((int) ($counts[$severity] ?? 0) > 0) {
                return true;
            }
        }

        foreach (['critical_issues', 'high_issues', 'critical_count', 'high_count'] as $key) {
            if ((int) ($result[$key] ?? 0) > 0) {
                return true;
            }
        }

        $issues = isset($result['issues']) && is_array($result['issues']) ? $result['issues'] : [];
        foreach ($issues as $issue) {
            if (!is_array($issue)) {
                continue;
            }

            $severity = strtolower(trim((string) ($issue['severity'] ?? '')));
            if (in_array($severity, ['critical', 'high'], true)) {
                return true;
            }
        }

        return false;
    }

    /**
     * @return array<int,string>
     */
    private function notificationRecipients(string $type): array
    {
        $recipients = $this->settings->recipients($type);

        if (!empty($recipients)) {
            return $recipients;
        }

        $adminEmail = function_exists('get_option') ? (string) get_option('admin_email', '') : '';
        $adminEmail = function_exists('sanitize_email') ? sanitize_email($adminEmail) : $adminEmail;

        return filter_var($adminEmail, FILTER_VALIDATE_EMAIL) ? [$adminEmail] : [];
    }

    /**
     * @param array<string,mixed> $result
     */
    private function mailSubject(array $result): string
    {
        $status = (string) ($result['status'] ?? '');
        $issuesFound = (int) ($result['issues_found'] ?? 0);
        $siteName = function_exists('get_bloginfo') ? (string) get_bloginfo('name') : 'WordPress';
        $scanType = self::scanTypeLabel((string) ($result['scan_type'] ?? ScheduledScanSettings::TYPE_FILE));

        if ($status === 'failed') {
            return sprintf('[SHIELD-X] Scheduled %s scan failed on %s', $scanType, $siteName);
        }

        if ($issuesFound > 0) {
            return sprintf('[SHIELD-X] Scheduled %s scan found %d issue(s) on %s', $scanType, $issuesFound, $siteName);
        }

        return sprintf('[SHIELD-X] Scheduled %s scan completed on %s', $scanType, $siteName);
    }

    /**
     * @param array<string,mixed> $result
     */
    private function mailContent(array $result): string
    {
        $scanId = (string) ($result['scan_id'] ?? '');
        $type = ScheduledScanSettings::sanitizeType((string) ($result['scan_type'] ?? ScheduledScanSettings::TYPE_FILE));
        $status = (string) ($result['status'] ?? 'unknown');
        $issuesFound = (int) ($result['issues_found'] ?? 0);
        $failed = $status === 'failed';
        $scope = (string) ($result['scope_label'] ?? '');
        if ($scope === '' && isset($this->settings)) {
            $scope = implode(', ', $this->settings->scopes($type));
        }

        $title = $failed ? 'Scheduled scan failed' : ($issuesFound > 0 ? 'Scheduled scan found issues' : 'Scheduled scan completed');
        $message = $failed
            ? 'The scheduled scan could not complete. Review the error and run the scan again after fixing the cause.'
            : ($issuesFound > 0 ? 'SHIELD-X found findings that need review in the scanner results.' : 'The scheduled scan completed without new findings.');

        $html = EmailTemplate::notice($title, $message, $failed ? 'critical' : ($issuesFound > 0 ? 'warning' : 'success'));
        $details = [
            'Type' => self::scanTypeLabel($type),
            'Status' => $status,
            'Scan ID' => $scanId !== '' ? $scanId : 'n/a',
            'Scope' => $scope !== '' ? $scope : 'n/a',
            'Files total' => (string) ($result['files_total'] ?? 0),
            'Files scanned' => (string) ($result['files_scanned'] ?? 0),
            'Files skipped' => (string) ($result['files_skipped'] ?? 0),
            'Hash checks' => (string) ($result['files_hash_checked'] ?? 0),
            'Database checks' => (string) ($result['database_items_scanned'] ?? 0),
            'Issues found' => (string) $issuesFound,
        ];

        if (!empty($result['error'])) {
            $details['Error'] = (string) $result['error'];
        }

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

        $detailUrl = $scanId !== '' && function_exists('admin_url')
            ? admin_url('admin.php?page=shield-x-scan&shieldx_scan_id=' . rawurlencode($scanId))
            : '';

        if ($detailUrl !== '') {
            $html .= EmailTemplate::button('View scan details', $detailUrl);
        }

        return $html;
    }

    private static function scanTypeLabel(string $type): string
    {
        $type = ScheduledScanSettings::sanitizeType($type);

        if ($type === ScheduledScanSettings::TYPE_DATABASE) {
            return 'database';
        }

        if ($type === ScheduledScanSettings::TYPE_VULNERABILITY) {
            return 'vulnerability';
        }

        return 'file';
    }

    /**
     * @param array<string,mixed> $context
     */
    private function log(string $level, string $event, array $context): void
    {
        if (!$this->logger) {
            return;
        }

        if ($level === 'warning') {
            $this->logger->warning($event, $context);
            return;
        }

        $this->logger->info($event, $context);
    }

    private static function formatTimestamp(int $timestamp): string
    {
        if ($timestamp <= 0) {
            return '';
        }

        return function_exists('wp_date') ? wp_date('Y-m-d H:i:s', $timestamp) : gmdate('Y-m-d H:i:s', $timestamp);
    }

    private static function intervalForRecurrence(string $recurrence): int
    {
        $hour = defined('HOUR_IN_SECONDS') ? (int) HOUR_IN_SECONDS : 3600;
        $day = defined('DAY_IN_SECONDS') ? (int) DAY_IN_SECONDS : 86400;

        switch ($recurrence) {
            case 'hourly':
                return $hour;
            case 'twicedaily':
                return 12 * $hour;
            case 'weekly':
                return 7 * $day;
            case 'daily':
            default:
                return $day;
        }
    }
}
