<?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 ScheduledRestorePointRunner
{
    public const HOOK = 'shieldx_scheduled_restore_point';
    private const LOCK_KEY = 'shieldx_scheduled_restore_point_lock';
    private const LOCK_TTL = 3600;
    private const PROGRESS_OPTION = 'shieldx_scheduled_restore_point_progress';
    private const PROGRESS_CONTINUATION_ARG = 'progress';
    private const PROGRESS_CONTINUATION_DELAY = 60;
    private const PROGRESS_BATCH_FILES = 500;
    private const PROGRESS_MAX_SECONDS = 8;

    /** @var RestorePointManager */
    private $restorePoints;

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

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

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

    public function __construct(RestorePointManager $restorePoints, ScheduledRestorePointSettings $settings, ?AuditLogger $logger = null)
    {
        $this->restorePoints = $restorePoints;
        $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(ScheduledRestorePointSettings $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()) {
            self::clearScheduledEvents();
            return;
        }

        if (!$settings->enabled()) {
            self::clearScheduledEvents();
            return;
        }

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

        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
    {
        self::clearScheduledEvents();
        self::deleteProgressState();
        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', 'restore_point.schedule_skipped', ['reason' => 'lock_active']);

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

                return $result;
            }
            $lockAcquired = true;

            $started = microtime(true);
            $progress = $this->runProgressiveRestorePoint();
            $result = $this->normalizeResult($progress, $started);

            if ((string) ($result['status'] ?? '') === 'running') {
                $this->safeRecordRun($result, false);
                $this->safeScheduleContinuation();
                $this->safeLog('info', 'restore_point.scheduled_continuing', [
                    'restore_id' => (string) ($result['restore_id'] ?? ''),
                    'percent' => (int) ($result['percent'] ?? 0),
                    'files' => (int) ($result['files'] ?? 0),
                ]);

                return $result;
            }

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

            $this->safeLog($result['status'] === 'failed' ? 'warning' : 'info', $result['status'] === 'failed' ? 'restore_point.scheduled_failed' : 'restore_point.scheduled_completed', [
                'restore_id' => (string) ($result['restore_id'] ?? ''),
                'status' => (string) ($result['status'] ?? ''),
                'files' => (int) ($result['files'] ?? 0),
                'bytes_exported' => (int) ($result['bytes_exported'] ?? 0),
                'database_backup_id' => (string) ($result['database_backup_id'] ?? ''),
                'warning_count' => (int) ($result['warning_count'] ?? 0),
                'notified' => $notified,
            ]);

            return $result;
        } catch (\Throwable $error) {
            $result = [
                'status' => 'failed',
                'restore_id' => '',
                'files' => 0,
                'bytes_exported' => 0,
                'database_backup_id' => '',
                'truncated' => false,
                'warning_count' => 0,
                'duration_ms' => 0,
                'error' => $error->getMessage(),
            ];
            $notified = $this->safeSendNotification($result);
            $this->safeRecordRun($result, $notified);
            $this->safeLog('warning', 'restore_point.scheduled_failed', [
                'error' => $error->getMessage(),
                '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', 'restore_point.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', 'restore_point.record_run_failed', ['error' => $error->getMessage()]);
        }
    }

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

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

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

    /**
     * @param array<string,mixed> $progress
     * @return array<string,mixed>
     */
    private function normalizeResult(array $progress, float $started): array
    {
        $status = (string) ($progress['status'] ?? '');
        if (!in_array($status, ['running', 'completed', 'failed'], true)) {
            $status = !empty($progress['ok']) ? 'completed' : 'failed';
        }
        $truncated = !empty($progress['truncated']);
        $databaseWarningDetails = $this->sanitizeWarningDetails((array) ($progress['database_warning_details'] ?? []));
        $databaseWarningCount = max(0, (int) ($progress['database_warning_count'] ?? count($databaseWarningDetails)));
        $warningDetails = $databaseWarningDetails;
        if ($truncated) {
            array_unshift($warningDetails, 'The file restore point hit the shared-hosting safety cap.');
        }

        return [
            'status' => $status,
            'restore_id' => (string) ($progress['restore_id'] ?? ''),
            'files' => (int) ($progress['files'] ?? 0),
            'bytes_exported' => (int) ($progress['bytes_exported'] ?? 0),
            'database_backup_id' => (string) ($progress['database_backup_id'] ?? ''),
            'truncated' => $truncated,
            'warning_count' => ($truncated ? 1 : 0) + $databaseWarningCount,
            'warning_details' => $this->sanitizeWarningDetails($warningDetails),
            'duration_ms' => (int) round((microtime(true) - $started) * 1000),
            'error' => $status === 'failed' ? (string) ($progress['error'] ?? $progress['message'] ?? 'Restore point failed.') : '',
            'message' => (string) ($progress['message'] ?? ''),
            'percent' => (int) ($progress['percent'] ?? ($status === 'completed' ? 100 : 0)),
            'scheduled_progressive' => true,
        ];
    }

    /**
     * @return array<string,mixed>
     */
    private function runProgressiveRestorePoint(): array
    {
        $state = self::readProgressState();
        $restoreId = (string) ($state['restore_id'] ?? '');

        if ($restoreId === '') {
            $started = $this->restorePoints->startProgressiveCreate('scheduled_restore_point', [], 0, $this->settings->includeDatabase());
            $restoreId = (string) ($started['restore_id'] ?? '');
            if ($restoreId === '' || (string) ($started['status'] ?? '') === 'failed') {
                self::deleteProgressState();

                return $started;
            }

            self::writeProgressState($restoreId);
        }

        $progress = $this->restorePoints->runProgressiveCreateBatch($restoreId, self::PROGRESS_BATCH_FILES, self::PROGRESS_MAX_SECONDS);
        $status = (string) ($progress['status'] ?? 'failed');

        if ($status === 'running') {
            self::writeProgressState($restoreId);
            $progress['reason'] = 'scheduled_restore_point_continuing';

            return $progress;
        }

        self::deleteProgressState();

        return $progress;
    }

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

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

        if (function_exists('wp_next_scheduled') && wp_next_scheduled(self::HOOK, [self::PROGRESS_CONTINUATION_ARG])) {
            return;
        }

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

    private static function clearScheduledEvents(): void
    {
        if (!function_exists('wp_clear_scheduled_hook')) {
            return;
        }

        wp_clear_scheduled_hook(self::HOOK, []);
        wp_clear_scheduled_hook(self::HOOK, [self::PROGRESS_CONTINUATION_ARG]);
    }

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

        $state = get_option(self::PROGRESS_OPTION, []);

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

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

        update_option(self::PROGRESS_OPTION, [
            'restore_id' => $restoreId,
            'updated_at' => self::formatTimestamp(time()),
        ], false);
    }

    private static function deleteProgressState(): void
    {
        if (function_exists('delete_option')) {
            delete_option(self::PROGRESS_OPTION);
        }
    }

    /**
     * @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';
        $warningCount = (int) ($result['warning_count'] ?? 0);
        $critical = $failed;
        $shouldSend = $this->settings->emailReportsEnabled()
            || ($warningCount > 0 && $this->settings->alertOnWarning())
            || ($failed && $this->settings->alertOnFailure());

        if (!$critical || !$shouldSend || !NotificationManager::allowsImmediateMail($critical)) {
            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 restore point report'),
            EmailTemplate::headers()
        );
    }

    /**
     * @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 restore point failed on %s', $siteName);
        }

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

        return sprintf('[SHIELD-X] Scheduled restore point 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);
        $restoreId = (string) ($result['restore_id'] ?? '');
        $databaseBackupId = (string) ($result['database_backup_id'] ?? '');

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

        $html = EmailTemplate::notice($title, $message, $failed ? 'critical' : ($warningCount > 0 ? 'warning' : 'success'));
        $details = [
            'Status' => $status,
            'Restore point ID' => $restoreId !== '' ? $restoreId : 'n/a',
            'Files captured' => (string) ($result['files'] ?? 0),
            'Bytes exported' => (string) ($result['bytes_exported'] ?? 0),
            'Database backup' => $databaseBackupId !== '' ? $databaseBackupId : 'n/a',
            '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#shieldx-restore-points') : '';
        if ($detailUrl !== '') {
            $html .= EmailTemplate::button('View restore points', $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;
        }
    }
}
