<?php

declare(strict_types=1);

namespace ShieldX\Maintenance;

defined('ABSPATH') || exit;

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

final class ScheduledBackupRunner
{
    public const HOOK = 'shieldx_scheduled_backup';
    private const LOCK_KEY = 'shieldx_scheduled_backup_lock';
    private const LOCK_TTL = 1800;

    /** @var DatabaseBackupManager */
    private $backupManager;

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

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

    /** @var string */
    private $lockOwnerToken = '';

    public function __construct(DatabaseBackupManager $backupManager, ScheduledBackupSettings $settings, ?AuditLogger $logger = null)
    {
        $this->backupManager = $backupManager;
        $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(ScheduledBackupSettings $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, []);
            return;
        }

        if (!$settings->enabled()) {
            wp_clear_scheduled_hook(self::HOOK, []);
            return;
        }

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

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

    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, []);
        }

        self::releaseLock();
    }

    /**
     * @return array<string,mixed>
     */
    public static function scheduleState(): array
    {
        $next = function_exists('wp_next_scheduled') ? (int) wp_next_scheduled(self::HOOK, []) : 0;
        $schedule = function_exists('wp_get_schedule') ? (string) wp_get_schedule(self::HOOK, []) : '';

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

    public static function isLocked(): bool
    {
        return self::lockState() !== [];
    }

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

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

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

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

                return $result;
            }
            $lockAcquired = true;

            $created = $this->backupManager->create([], $this->settings->maxRowsPerTable());
            $result = $this->normalizeResult($created);
            $notified = $this->safeSendNotification($result);
            $reportRequested = $this->reportRequested($result);
            $this->safeRecordRun($result, $notified);
            if (!SchedulerBridge::controlsTask('core.backup.database')) {
                $this->safeEnsureScheduled();
            }

            $this->safeLog($result['status'] === 'failed' ? 'warning' : 'info', $result['status'] === 'failed' ? 'backup.scheduled_failed' : 'backup.scheduled_completed', [
                'backup_id' => (string) ($result['backup_id'] ?? ''),
                'status' => (string) ($result['status'] ?? ''),
                'tables' => (int) ($result['tables'] ?? 0),
                'rows_exported' => (int) ($result['rows_exported'] ?? 0),
                'warning_count' => (int) ($result['warning_count'] ?? 0),
                'report_requested' => $reportRequested,
                'notified' => $notified,
            ]);

            return $result;
        } catch (\Throwable $error) {
            $result = [
                'status' => 'failed',
                'backup_id' => '',
                'tables' => 0,
                'rows_exported' => 0,
                'size' => 0,
                'duration_ms' => 0,
                'warning_count' => 0,
                'error' => $error->getMessage(),
            ];
            $notified = $this->safeSendNotification($result);
            $reportRequested = $this->reportRequested($result);
            $this->safeRecordRun($result, $notified);
            $this->safeLog('warning', 'backup.scheduled_failed', [
                'error' => $error->getMessage(),
                'report_requested' => $reportRequested,
                'notified' => $notified,
            ]);

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

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

            return false;
        }
    }

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

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

    private function safeEnsureScheduled(): void
    {
        try {
            self::ensureScheduled($this->settings);
        } catch (\Throwable $error) {
            $this->safeLog('warning', 'backup.reschedule_failed', ['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 backup log failed for %s: %s', $event, $error->getMessage()));
        }
    }

    /**
     * @param array<string,mixed> $created
     * @return array<string,mixed>
     */
    private function normalizeResult(array $created): array
    {
        $ok = !empty($created['ok']);
        $warningCount = max(0, (int) ($created['warning_count'] ?? $created['truncated_tables'] ?? 0));

        return [
            'status' => $ok ? 'completed' : 'failed',
            'backup_id' => (string) ($created['backup_id'] ?? ''),
            'tables' => (int) ($created['tables'] ?? 0),
            'rows_exported' => (int) ($created['rows_exported'] ?? 0),
            'size' => (int) ($created['size'] ?? 0),
            'duration_ms' => (int) ($created['duration_ms'] ?? 0),
            'warning_count' => $warningCount,
            'truncated_tables' => max(0, (int) ($created['truncated_tables'] ?? $warningCount)),
            'warning_details' => $this->sanitizeWarningDetails((array) ($created['warning_details'] ?? [])),
            'error' => $ok ? '' : (string) ($created['message'] ?? 'Database backup failed.'),
            'message' => (string) ($created['message'] ?? ''),
        ];
    }

    private function acquireLock(): bool
    {
        $token = bin2hex(random_bytes(16));
        if (function_exists('add_option') && function_exists('get_option') && function_exists('delete_option')) {
            $state = [
                'owner' => $token,
                'expires' => time() + self::LOCK_TTL,
            ];

            if (add_option(self::LOCK_KEY, $state, '', 'no')) {
                $this->lockOwnerToken = $token;

                return true;
            }

            $existing = self::lockState();
            if (!empty($existing) && (int) ($existing['expires'] ?? 0) < time()) {
                delete_option(self::LOCK_KEY);
                if (add_option(self::LOCK_KEY, $state, '', 'no')) {
                    $this->lockOwnerToken = $token;

                    return true;
                }
            }

            return false;
        }

        if (!function_exists('get_transient') || !function_exists('set_transient')) {
            return true;
        }

        if (get_transient(self::LOCK_KEY)) {
            return false;
        }

        set_transient(self::LOCK_KEY, $token, self::LOCK_TTL);
        $this->lockOwnerToken = $token;

        return true;
    }

    private static function releaseLock(): void
    {
        if (function_exists('delete_option')) {
            delete_option(self::LOCK_KEY);
        }

        if (function_exists('delete_transient')) {
            delete_transient(self::LOCK_KEY);
        }
    }

    private function releaseOwnedLock(): void
    {
        $token = $this->lockOwnerToken;
        $this->lockOwnerToken = '';
        if ($token === '') {
            return;
        }

        if (function_exists('get_option') && function_exists('delete_option')) {
            $state = get_option(self::LOCK_KEY, []);
            if (is_array($state) && (string) ($state['owner'] ?? '') === $token) {
                delete_option(self::LOCK_KEY);
            }
        }

        if (function_exists('get_transient') && function_exists('delete_transient') && get_transient(self::LOCK_KEY) === $token) {
            delete_transient(self::LOCK_KEY);
        }
    }

    /**
     * @return array<string,mixed>
     */
    private static function lockState(): array
    {
        if (function_exists('get_option')) {
            $state = get_option(self::LOCK_KEY, []);
            if (is_array($state) && (int) ($state['expires'] ?? 0) >= time()) {
                return $state;
            }
        }

        $transient = function_exists('get_transient') ? get_transient(self::LOCK_KEY) : false;

        return $transient ? ['owner' => (string) $transient, 'expires' => time() + self::LOCK_TTL] : [];
    }

    /**
     * @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';
        $shouldSend = $this->reportRequested($result);

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

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

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

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

    /**
     * @param array<string,mixed> $result
     */
    private function reportRequested(array $result): bool
    {
        $failed = (string) ($result['status'] ?? '') === 'failed';
        $warningCount = (int) ($result['warning_count'] ?? 0);

        return $this->settings->emailReportsEnabled()
            || ($warningCount > 0 && $this->settings->alertOnWarning())
            || ($failed && $this->settings->alertOnFailure());
    }

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

        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
    {
        $siteName = function_exists('get_bloginfo') ? (string) get_bloginfo('name') : 'WordPress';

        if ((string) ($result['status'] ?? '') === 'failed') {
            return sprintf('[SHIELD-X] Scheduled database backup failed on %s', $siteName);
        }

        if ((int) ($result['warning_count'] ?? 0) > 0) {
            return sprintf('[SHIELD-X] Scheduled database backup completed with warnings on %s', $siteName);
        }

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

    /**
     * @param array<string,mixed> $result
     */
    private function mailContent(array $result): string
    {
        $status = (string) ($result['status'] ?? 'unknown');
        $failed = $status === 'failed';
        $warningCount = (int) ($result['warning_count'] ?? 0);
        $backupId = (string) ($result['backup_id'] ?? '');

        $title = $failed ? 'Database backup failed' : ($warningCount > 0 ? 'Database backup completed with warnings' : 'Database backup completed');
        $message = $failed
            ? 'The scheduled database backup could not complete. Review the error before relying on this backup window.'
            : ($warningCount > 0 ? 'The database backup finished, but SHIELD-X recorded warnings that should be reviewed.' : 'The scheduled database backup completed successfully.');

        $html = EmailTemplate::notice($title, $message, $failed ? 'critical' : ($warningCount > 0 ? 'warning' : 'success'));
        $details = [
            'Status' => $status,
            'Backup ID' => $backupId !== '' ? $backupId : 'n/a',
            'Tables' => (string) ($result['tables'] ?? 0),
            'Rows exported' => (string) ($result['rows_exported'] ?? 0),
            'Size' => (string) ($result['size'] ?? 0) . ' bytes',
            'Warnings' => (string) $warningCount,
        ];

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

        $warningDetails = $this->sanitizeWarningDetails((array) ($result['warning_details'] ?? []));
        if ($warningCount > 0 && !empty($warningDetails)) {
            $details['Warning details'] = implode('; ', $warningDetails);
        }

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

        $detailUrl = function_exists('admin_url') ? admin_url('admin.php?page=shield-x-backup') : '';
        if ($detailUrl !== '') {
            $html .= EmailTemplate::button('View backups', $detailUrl);
        }

        return $html;
    }

    /**
     * @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);
    }

    /**
     * @param array<int,mixed> $warnings
     * @return array<int,string>
     */
    private function sanitizeWarningDetails(array $warnings): array
    {
        $clean = [];
        foreach ($warnings as $warning) {
            $warning = trim(preg_replace('/[\r\n\t]+/', ' ', (string) $warning) ?: '');
            if ($warning === '') {
                continue;
            }

            $clean[] = substr($warning, 0, 240);
            if (count($clean) >= 20) {
                break;
            }
        }

        return array_values(array_unique($clean));
    }

    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;
        }
    }
}
