<?php
declare(strict_types=1);

namespace Help4BuilderSuite\Modules\Calendar;

use Help4BuilderSuite\Contracts\ModuleContract;
use Help4BuilderSuite\Support\RobotsContract;

if (! defined('ABSPATH')) {
    exit;
}

final class Module implements ModuleContract
{
    public const CAP_MANAGE = 'manage_help4_calendar';
    public const CAP_SOURCES = 'manage_help4_calendar_sources';

    private string $hook = '';

    public function key(): string { return 'calendar'; }

    public function boot(): void
    {
        add_action('init', [$this, 'ensureCapabilities'], 5);
        add_action('init', [$this, 'registerEventRoutes']);
        add_action('init', [$this, 'registerSitemapProvider'], 11);
        add_filter('query_vars', [$this, 'queryVars']);
        add_action('template_redirect', [$this, 'renderEventLeaf'], -80);
        add_filter('wp_robots', [$this, 'eventRobots']);
        add_action('admin_menu', [$this, 'menu']);
        add_action('admin_enqueue_scripts', [$this, 'assets']);
        add_action('wp_ajax_h4bs_calendar_save', [$this, 'save']);
        add_action('wp_ajax_h4bs_calendar_sync', [$this, 'sync']);
        add_action('wp_ajax_h4bs_calendar_recover', [$this, 'recover']);
        add_action('wp_enqueue_scripts', [$this, 'frontendAssets']);
        add_action('init', [LegacyPostBridge::class, 'refresh'], 30);
        add_action('save_post_bp_calendar_event', [LegacyPostBridge::class, 'markDirty']);
        add_action('before_delete_post', [LegacyPostBridge::class, 'markDeletedPostDirty']);
        add_action('added_post_meta', [LegacyPostBridge::class, 'markMetaDirty'], 10, 4);
        add_action('updated_post_meta', [LegacyPostBridge::class, 'markMetaDirty'], 10, 4);
        add_action('deleted_post_meta', [LegacyPostBridge::class, 'markMetaDirty'], 10, 4);
        add_action('h4bs_calendar_refresh_legacy', [LegacyPostBridge::class, 'forceRefresh']);
        $this->maybeMigrateLegacy();
    }

    public function registerEventRoutes(): void
    {
        add_rewrite_rule('^events/([^/]+)/?$', 'index.php?h4bs_event_slug=$matches[1]', 'top');
        if ((string) get_option('h4bs_event_rewrite_version', '') !== H4BS_VERSION) {
            flush_rewrite_rules(false);
            update_option('h4bs_event_rewrite_version', H4BS_VERSION, false);
        }
    }

    /** @param array<int, string> $vars @return array<int, string> */
    public function queryVars(array $vars): array
    {
        $vars[] = 'h4bs_event_slug';
        return $vars;
    }

    public function renderEventLeaf(): void
    {
        $slug = sanitize_title((string) get_query_var('h4bs_event_slug'));
        if ($slug === '') return;
        $event = Workspace::eventBySlug($slug);
        if (! is_array($event)) {
            global $wp_query;
            $wp_query->set_404();
            status_header(404);
            nocache_headers();
            return;
        }
        status_header(200);
        $canonical = Workspace::eventUrl($event);
        $schema = $this->eventSchema($event, $canonical);
        ?><!doctype html><html <?php language_attributes(); ?>><head><meta charset="<?php bloginfo('charset'); ?>"><meta name="viewport" content="width=device-width,initial-scale=1"><title><?php echo esc_html((string) $event['title'] . ' | ' . get_bloginfo('name')); ?></title><link rel="canonical" href="<?php echo esc_url($canonical); ?>"><?php wp_head(); ?><?php if ($schema !== []) : ?><?php wp_print_inline_script_tag(wp_json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT), ['type' => 'application/ld+json', 'class' => 'h4bs-event-schema']); ?><?php endif; ?></head><body <?php body_class('h4bs-event-leaf'); ?>><?php wp_body_open(); ?><main class="h4bs-event-leaf-main"><article class="h4bs-event-leaf-card"><p class="h4bs-event-kicker"><?php esc_html_e('Event', 'help4-builder-suite'); ?></p><h1><?php echo esc_html((string) $event['title']); ?></h1><p><time datetime="<?php echo esc_attr((string) $event['start']); ?>"><?php echo esc_html((string) $event['start']); ?></time></p><?php if ((string) $event['description'] !== '') : ?><div><?php echo wp_kses_post(wpautop(esc_html((string) $event['description']))); ?></div><?php endif; ?><?php if ((string) $event['venue_name'] !== '') : ?><p><?php echo esc_html((string) $event['venue_name']); ?></p><?php endif; ?><?php if ((string) $event['ticket_url'] !== '') : ?><p><a class="h4bs-event-ticket" href="<?php echo esc_url((string) $event['ticket_url']); ?>"><?php esc_html_e('Registration and tickets', 'help4-builder-suite'); ?></a></p><?php endif; ?></article></main><?php wp_footer(); ?></body></html><?php
        exit;
    }

    /** @param array<string, bool> $robots @return array<string, bool> */
    public function eventRobots(array $robots): array
    {
        $slug = sanitize_title((string) get_query_var('h4bs_event_slug'));
        if ($slug === '') return $robots;
        $event = Workspace::eventBySlug($slug);
        if (is_array($event) && ! Workspace::searchReadiness($event)['eligible']) {
            $robots['noindex'] = true;
            $robots['follow'] = true;
            unset($robots['index']);
        }
        return $robots;
    }

    /** @param array<string, mixed> $event @return array<string, mixed> */
    private function eventSchema(array $event, string $canonical): array
    {
        if (! Workspace::searchReadiness($event)['eligible']) return [];
        if (! RobotsContract::isIndexable()) return [];
        $status = ['scheduled'=>'EventScheduled','cancelled'=>'EventCancelled','postponed'=>'EventPostponed','rescheduled'=>'EventRescheduled','moved_online'=>'EventMovedOnline'][(string) $event['event_status']];
        $start = $this->dateWithTimezone((string) $event['start'], (string) $event['timezone']);
        if ($start === '') return [];
        $mode = ['offline'=>'OfflineEventAttendanceMode','online'=>'OnlineEventAttendanceMode','mixed'=>'MixedEventAttendanceMode'][(string) $event['attendance_mode']];
        $schema = ['@context'=>'https://schema.org','@type'=>'Event','@id'=>$canonical . '#event','url'=>$canonical,'name'=>(string) $event['title'],'description'=>(string) $event['description'],'startDate'=>$start,'eventStatus'=>'https://schema.org/' . $status,'eventAttendanceMode'=>'https://schema.org/' . $mode,'location'=>['@type'=>'Place','name'=>(string) $event['venue_name'],'address'=>['@type'=>'PostalAddress','streetAddress'=>(string) $event['street_address'],'addressLocality'=>(string) $event['address_locality'],'addressRegion'=>(string) $event['address_region'],'postalCode'=>(string) $event['postal_code'],'addressCountry'=>(string) $event['address_country']]]];
        $end = $this->dateWithTimezone((string) $event['end'], (string) $event['timezone']);
        if ($end !== '') $schema['endDate'] = $end;
        if ((string) $event['organizer_name'] !== '') $schema['organizer'] = ['@type'=>'Organization','name'=>(string) $event['organizer_name'],'url'=>(string) $event['organizer_url']];
        if ((string) $event['ticket_url'] !== '') {
            $schema['offers'] = ['@type'=>'Offer','url'=>(string) $event['ticket_url'],'availability'=>'https://schema.org/InStock'];
            if ($event['offer_price'] !== '') {
                $schema['offers']['price'] = $event['offer_price'];
                $schema['offers']['priceCurrency'] = (string) $event['offer_currency'];
            }
        }
        return (array) apply_filters('h4bs_calendar_event_schema', $schema, $event);
    }

    public function registerSitemapProvider(): void
    {
        if (! function_exists('wp_sitemaps_get_server')) return;
        wp_sitemaps_get_server()->registry->add_provider('events', new EventSitemapProvider());
    }

    private function dateWithTimezone(string $value, string $timezone): string
    {
        if ($value === '') return '';
        try {
            $zone = $timezone !== '' ? new \DateTimeZone($timezone) : wp_timezone();
            return (new \DateTimeImmutable($value, $zone))->format(DATE_W3C);
        } catch (\Exception $exception) {
            return '';
        }
    }

    public function menu(): void
    {
        $this->hook = (string) add_submenu_page('h4bs-settings', __('Calendar Workspace', 'help4-builder-suite'), __('Calendar', 'help4-builder-suite'), self::CAP_MANAGE, 'h4bs-calendar', [$this, 'page']);
        if ($this->hook === '') $this->hook = (string) add_menu_page(__('Help4 Calendar', 'help4-builder-suite'), __('Help4 Calendar', 'help4-builder-suite'), self::CAP_MANAGE, 'h4bs-calendar', [$this, 'page'], 'dashicons-calendar-alt', 58);
    }

    /** Give administrators the Calendar capabilities without granting them to other roles. */
    public function ensureCapabilities(): void
    {
        $role = get_role('administrator');
        if (! $role) return;
        foreach ([self::CAP_MANAGE, self::CAP_SOURCES] as $capability) {
            if (! $role->has_cap($capability)) $role->add_cap($capability);
        }
    }

    public function assets(string $hook): void
    {
        if ($hook !== $this->hook) return;
        $workspace = Workspace::load();
        $canManageSources = current_user_can(self::CAP_SOURCES);
        if (! $canManageSources) $workspace['sources'] = [];
        wp_enqueue_style('h4bs-calendar-admin', H4BS_URL . 'assets/css/calendar-admin.css', [], H4BS_VERSION);
        wp_enqueue_script('h4bs-calendar-admin', H4BS_URL . 'assets/js/calendar-admin.js', [], H4BS_VERSION, ['in_footer' => true, 'strategy' => 'defer']);
        wp_localize_script('h4bs-calendar-admin', 'h4bsCalendarData', [
            'ajaxUrl' => admin_url('admin-ajax.php'),
            'nonce' => wp_create_nonce('h4bs_calendar'),
            'workspace' => $workspace,
            'canManageSources' => $canManageSources,
            'recovery' => $this->adminRecoveryPayload(),
        ]);
    }

    public function frontendAssets(): void
    {
        if (! $this->needsFrontendAssets()) return;
        wp_enqueue_style('h4bs-calendar', H4BS_URL . 'assets/css/calendar.css', [], H4BS_VERSION);
        wp_enqueue_script('h4bs-calendar', H4BS_URL . 'assets/js/calendar.js', [], H4BS_VERSION, ['in_footer' => true, 'strategy' => 'defer']);
    }

    private function needsFrontendAssets(): bool
    {
        if (sanitize_title((string) get_query_var('h4bs_event_slug')) !== '') return true;
        if (! is_singular()) return false;
        $postId = get_queried_object_id();
        if ($postId < 1) return false;
        $layout = (string) get_post_meta($postId, '_h4_builder_layout', true);
        if ($layout !== '' && preg_match('/["\']type["\']\s*:\s*["\']calendar["\']/', $layout) === 1) return true;
        $content = (string) get_post_field('post_content', $postId);
        return has_shortcode($content, 'h4_calendar') || has_shortcode($content, 'h4bs_calendar');
    }

    public function page(): void
    {
        if (! current_user_can(self::CAP_MANAGE)) wp_die(esc_html__('You cannot manage calendars.', 'help4-builder-suite'));
        $canManageSources = current_user_can(self::CAP_SOURCES);
        ?>
        <div class="wrap h4bs-calendar-admin" data-h4bs-calendar-admin>
            <header class="h4bs-calendar-header">
                <div><h1><?php esc_html_e('Calendar Workspace', 'help4-builder-suite'); ?></h1><p><?php echo esc_html($canManageSources ? __('Connect sources, review sync health, add local events, and control what appears without editing upstream calendars.', 'help4-builder-suite') : __('Review synced events, add local events, and control what appears. Connected source settings remain protected.', 'help4-builder-suite')); ?></p></div>
                <div class="h4bs-calendar-commands"><span data-h4bs-calendar-save-state data-state="clean" role="status" aria-live="polite"><?php esc_html_e('All changes saved', 'help4-builder-suite'); ?></span><button type="button" class="button" data-calendar-undo disabled><?php esc_html_e('Undo', 'help4-builder-suite'); ?></button><button type="button" class="button" data-calendar-redo disabled><?php esc_html_e('Redo', 'help4-builder-suite'); ?></button><button type="button" class="button" data-h4bs-calendar-save disabled><?php esc_html_e('Save calendar', 'help4-builder-suite'); ?></button></div>
            </header>
            <nav class="nav-tab-wrapper" aria-label="<?php esc_attr_e('Calendar workspace sections', 'help4-builder-suite'); ?>">
                <button type="button" class="nav-tab nav-tab-active" data-calendar-tab="events"><?php esc_html_e('Events', 'help4-builder-suite'); ?></button>
                <?php if ($canManageSources) : ?><button type="button" class="nav-tab" data-calendar-tab="sources"><?php esc_html_e('Sources & sync', 'help4-builder-suite'); ?></button><?php endif; ?>
                <button type="button" class="nav-tab" data-calendar-tab="presentation"><?php esc_html_e('Builder presentation', 'help4-builder-suite'); ?></button>
                <button type="button" class="nav-tab" data-calendar-tab="readiness"><?php esc_html_e('Search readiness', 'help4-builder-suite'); ?></button>
                <button type="button" class="nav-tab" data-calendar-tab="recovery"><?php esc_html_e('Recovery', 'help4-builder-suite'); ?></button>
            </nav>
            <main>
                <section data-calendar-panel="events"><div class="h4bs-calendar-toolbar"><label><span><?php esc_html_e('Search events', 'help4-builder-suite'); ?></span><input type="search" data-calendar-search></label><label><span><?php esc_html_e('Show', 'help4-builder-suite'); ?></span><select data-calendar-provenance><option value=""><?php esc_html_e('All events', 'help4-builder-suite'); ?></option><option value="source"><?php esc_html_e('Synced', 'help4-builder-suite'); ?></option><option value="local"><?php esc_html_e('Local', 'help4-builder-suite'); ?></option><option value="hidden"><?php esc_html_e('Hidden', 'help4-builder-suite'); ?></option></select></label><button type="button" class="button button-primary" data-calendar-add><?php esc_html_e('Add local event', 'help4-builder-suite'); ?></button></div><div data-calendar-event-list></div><p data-calendar-empty hidden><?php esc_html_e('No events match this view.', 'help4-builder-suite'); ?></p></section>
                <?php if ($canManageSources) : ?><section data-calendar-panel="sources" hidden><div data-calendar-source-list></div><button type="button" class="button" data-calendar-add-source><?php esc_html_e('Add source', 'help4-builder-suite'); ?></button></section><?php endif; ?>
                <section data-calendar-panel="presentation" hidden><h2><?php esc_html_e('Builder presentation', 'help4-builder-suite'); ?></h2><p><?php esc_html_e('Add the Calendar element in Builder, select Calendar or List, choose sources, and configure search, filters, responsive columns, labels, and empty text in the normal inspector.', 'help4-builder-suite'); ?></p><div class="h4bs-calendar-preview" data-calendar-preview></div></section>
                <section data-calendar-panel="readiness" hidden><h2><?php esc_html_e('Event search readiness', 'help4-builder-suite'); ?></h2><p><?php esc_html_e('Only complete, factual events are indexable or included in the event sitemap. Fix every listed field before expecting Event schema eligibility.', 'help4-builder-suite'); ?></p><table class="widefat striped"><thead><tr><th><?php esc_html_e('Event', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Status', 'help4-builder-suite'); ?></th><th><?php esc_html_e('Blocking fields', 'help4-builder-suite'); ?></th></tr></thead><tbody><?php foreach (Workspace::effectiveEvents() as $event) : $ready = Workspace::searchReadiness($event); ?><tr><th scope="row"><a href="<?php echo esc_url(Workspace::eventUrl($event)); ?>"><?php echo esc_html((string) $event['title']); ?></a></th><td><strong><?php echo esc_html($ready['eligible'] ? __('Ready', 'help4-builder-suite') : __('Not indexable', 'help4-builder-suite')); ?></strong></td><td><?php echo esc_html($ready['eligible'] ? __('None', 'help4-builder-suite') : implode(', ', $ready['missing'])); ?></td></tr><?php endforeach; ?></tbody></table></section>
                <section data-calendar-panel="recovery" hidden><h2><?php esc_html_e('Workspace recovery', 'help4-builder-suite'); ?></h2><p><?php esc_html_e('Each changed save keeps a bounded prior workspace revision. Recovery never alters upstream Google events.', 'help4-builder-suite'); ?></p><div data-calendar-recovery></div></section>
            </main>
            <dialog data-calendar-editor><form method="dialog" data-calendar-editor-form><header><h2 data-calendar-editor-title><?php esc_html_e('Edit event', 'help4-builder-suite'); ?></h2><button type="button" class="button-link" data-calendar-editor-close aria-label="<?php esc_attr_e('Close event editor', 'help4-builder-suite'); ?>">×</button></header><div data-calendar-editor-fields></div><footer><span data-calendar-editor-status role="status" aria-live="polite"></span><button type="button" class="button" data-calendar-editor-cancel><?php esc_html_e('Cancel', 'help4-builder-suite'); ?></button><button type="submit" class="button button-primary"><?php esc_html_e('Save event', 'help4-builder-suite'); ?></button></footer></form></dialog>
        </div>
        <?php
    }

    public function save(): void
    {
        $this->authorize();
        // phpcs:disable WordPress.Security.NonceVerification.Missing -- authorize() verifies the AJAX nonce and capability before request reads.
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Structured JSON is decoded then fully normalized by Workspace::sanitize().
        $raw = isset($_POST['workspace']) ? wp_unslash((string) $_POST['workspace']) : '';
        $decoded = json_decode($raw, true);
        if (! is_array($decoded)) wp_send_json_error(['message' => __('Invalid calendar data.', 'help4-builder-suite')], 422);
        $expected = max(1, absint(wp_unslash($_POST['revision'] ?? '0')));
        $requestId = isset($_POST['request_id']) ? sanitize_text_field(wp_unslash((string) $_POST['request_id'])) : '';
        if ($requestId !== '' && ! preg_match('/\A[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i', $requestId)) {
            wp_send_json_error(['message' => __('Invalid calendar save request. Reload and try again.', 'help4-builder-suite')], 422);
        }
        if ($requestId === '') $requestId = wp_generate_uuid4();
        $fingerprint = hash('sha256', $expected . "\n" . (string) wp_json_encode($decoded));
        $replayKey = 'h4bs_calendar_save_' . get_current_user_id() . '_' . hash('sha256', $requestId);
        $replay = get_transient($replayKey);
        if (is_array($replay)) {
            $priorFingerprint = (string) ($replay['fingerprint'] ?? '');
            if ($priorFingerprint === '' || ! hash_equals($priorFingerprint, $fingerprint)) {
                wp_send_json_error(['message' => __('This calendar save request was already used for different changes. Reload before saving.', 'help4-builder-suite')], 409);
            }
            $priorResponse = $replay['response'] ?? null;
            if (is_array($priorResponse)) {
                $priorResponse['replayed'] = true;
                wp_send_json_success($priorResponse);
            }
        }
        $current = Workspace::load();
        if ($expected !== (int) $current['revision']) wp_send_json_error(['message' => __('Calendar changed in another tab. Reload before saving.', 'help4-builder-suite'), 'workspace' => $this->adminWorkspacePayload($current)], 409);
        $history = get_option('h4bs_calendar_recovery', []);
        $history = is_array($history) ? $history : [];
        array_unshift($history, ['created_at' => current_time('mysql', true), 'user_id' => get_current_user_id(), 'workspace' => $current]);
        update_option('h4bs_calendar_recovery', array_slice($history, 0, 5), false);
        if (! current_user_can(self::CAP_SOURCES)) {
            $decoded['sources'] = $current['sources'];
            $decoded['source_events'] = $current['source_events'];
        }
        $decoded['revision'] = $expected + 1;
        $decoded['updated_at'] = current_time('mysql', true);
        $clean = Workspace::sanitize($decoded);
        update_option(Workspace::OPTION, $clean, false);
        do_action('h4bs_calendar_workspace_saved', $clean, $current);
        $response = ['workspace' => $this->adminWorkspacePayload($clean), 'recovery' => $this->adminRecoveryPayload($history), 'message' => __('Calendar saved.', 'help4-builder-suite'), 'replayed' => false];
        set_transient($replayKey, ['fingerprint' => $fingerprint, 'response' => $response], 10 * MINUTE_IN_SECONDS);
        wp_send_json_success($response);
        // phpcs:enable WordPress.Security.NonceVerification.Missing
    }

    public function recover(): void
    {
        $this->authorize();
        // phpcs:disable WordPress.Security.NonceVerification.Missing -- authorize() verifies the AJAX nonce and capability before request reads.
        $index = isset($_POST['index']) ? absint(wp_unslash($_POST['index'])) : 0;
        $history = get_option('h4bs_calendar_recovery', []);
        $entry = is_array($history) ? ($history[$index] ?? null) : null;
        if (! is_array($entry) || ! is_array($entry['workspace'] ?? null)) wp_send_json_error(['message' => __('Recovery point unavailable.', 'help4-builder-suite')], 404);
        $current = Workspace::load();
        $recovered = $entry['workspace'];
        if (! current_user_can(self::CAP_SOURCES)) {
            $recovered['sources'] = $current['sources'];
            $recovered['source_events'] = $current['source_events'];
        }
        $clean = Workspace::sanitize($recovered);
        $clean['revision'] = (int) $current['revision'] + 1;
        $clean['updated_at'] = current_time('mysql', true);
        update_option(Workspace::OPTION, $clean, false);
        wp_send_json_success(['workspace' => $this->adminWorkspacePayload($clean), 'recovery' => $this->adminRecoveryPayload($history), 'message' => __('Calendar recovery loaded and saved.', 'help4-builder-suite')]);
        // phpcs:enable WordPress.Security.NonceVerification.Missing
    }

    public function sync(): void
    {
        $this->authorize(self::CAP_SOURCES);
        // phpcs:disable WordPress.Security.NonceVerification.Missing -- authorize() verifies the AJAX nonce and capability before request reads.
        $sourceId = isset($_POST['source_id']) ? sanitize_key(wp_unslash((string) $_POST['source_id'])) : '';
        $data = Workspace::load();
        foreach ($data['sources'] as &$source) {
            if ($source['id'] !== $sourceId) continue;
            $result = $this->fetchSource($source);
            $source['last_sync_at'] = current_time('mysql', true);
            if (is_wp_error($result)) {
                $source['status'] = 'error';
                $source['last_error'] = $result->get_error_message();
                update_option(Workspace::OPTION, Workspace::sanitize($data), false);
                wp_send_json_error(['message' => $source['last_error'], 'workspace' => Workspace::sanitize($data)], 502);
            }
            $data['source_events'] = array_values(array_filter($data['source_events'], static fn(array $event): bool => $event['source_id'] !== $sourceId));
            $data['source_events'] = array_merge($data['source_events'], $result);
            $source['status'] = 'connected';
            $source['last_error'] = '';
            $source['last_sync_count'] = count($result);
            $data['revision']++;
            $data['updated_at'] = current_time('mysql', true);
            $clean = Workspace::sanitize($data);
            update_option(Workspace::OPTION, $clean, false);
            /* translators: %d: number of calendar events synchronized. */
            wp_send_json_success(['workspace' => $clean, 'message' => sprintf(__('Synced %d events.', 'help4-builder-suite'), count($result))]);
        }
        unset($source);
        wp_send_json_error(['message' => __('Calendar source not found.', 'help4-builder-suite')], 404);
        // phpcs:enable WordPress.Security.NonceVerification.Missing
    }

    /** @param array<string, mixed> $source @return array<int, array<string, mixed>>|\WP_Error */
    private function fetchSource(array $source)
    {
        $url = (string) $source['feed_url'];
        if ($source['provider'] === 'google' && $url === '') {
            $key = defined('H4BS_GOOGLE_CALENDAR_API_KEY') ? (string) H4BS_GOOGLE_CALENDAR_API_KEY : (string) apply_filters('h4bs_google_calendar_api_key', '');
            if ($key === '' || $source['calendar_id'] === '') return new \WP_Error('calendar_credentials', __('Google Calendar ID or API key is missing.', 'help4-builder-suite'));
            $url = add_query_arg(['key' => $key, 'singleEvents' => 'true', 'orderBy' => 'startTime', 'timeMin' => gmdate('c')], 'https://www.googleapis.com/calendar/v3/calendars/' . rawurlencode((string) $source['calendar_id']) . '/events');
        }
        if ($url === '') return new \WP_Error('calendar_url', __('Source URL is missing.', 'help4-builder-suite'));
        $response = wp_safe_remote_get($url, ['timeout' => 15, 'redirection' => 3, 'headers' => ['Accept' => 'application/json']]);
        if (is_wp_error($response)) return $response;
        $code = wp_remote_retrieve_response_code($response);
        if ($code < 200 || $code >= 300) {
            /* translators: %d: remote calendar HTTP status code. */
            return new \WP_Error('calendar_http', sprintf(__('Calendar source returned HTTP %d.', 'help4-builder-suite'), $code));
        }
        $body = json_decode(wp_remote_retrieve_body($response), true);
        $items = is_array($body['items'] ?? null) ? $body['items'] : (is_array($body) ? $body : []);
        $events = [];
        foreach ($items as $item) {
            if (! is_array($item)) continue;
            $id = sanitize_text_field((string) ($item['id'] ?? ''));
            $title = sanitize_text_field((string) ($item['summary'] ?? ($item['title'] ?? '')));
            if ($id === '' || $title === '') continue;
            $start = is_array($item['start'] ?? null) ? (string) (($item['start']['dateTime'] ?? $item['start']['date'] ?? '')) : (string) ($item['start'] ?? '');
            $end = is_array($item['end'] ?? null) ? (string) (($item['end']['dateTime'] ?? $item['end']['date'] ?? '')) : (string) ($item['end'] ?? '');
            $events[] = ['id' => $source['id'] . ':' . $id, 'source_id' => $source['id'], 'provenance' => 'source', 'title' => $title, 'description' => (string) ($item['description'] ?? ''), 'start' => $start, 'end' => $end, 'all_day' => strlen($start) === 10, 'location' => (string) ($item['location'] ?? ''), 'category' => '', 'badge' => '', 'url' => (string) ($item['htmlLink'] ?? ($item['url'] ?? '')), 'updated_at' => (string) ($item['updated'] ?? ''), 'extensions' => []];
        }
        return Workspace::sanitize(['source_events' => $events])['source_events'];
    }

    private function authorize(string $capability = self::CAP_MANAGE): void
    {
        check_ajax_referer('h4bs_calendar', 'nonce');
        if (! current_user_can($capability)) wp_send_json_error(['message' => __('Permission denied.', 'help4-builder-suite')], 403);
    }

    /** @param array<string, mixed> $workspace @return array<string, mixed> */
    private function adminWorkspacePayload(array $workspace): array
    {
        if (! current_user_can(self::CAP_SOURCES)) $workspace['sources'] = [];
        return $workspace;
    }

    /** @param array<int, mixed>|null $history @return array<int, array{created_at: string, user_id: int}> */
    private function adminRecoveryPayload(?array $history = null): array
    {
        $history = $history ?? (array) get_option('h4bs_calendar_recovery', []);
        return array_map(static function ($entry): array {
            return ['created_at' => sanitize_text_field((string) ($entry['created_at'] ?? '')), 'user_id' => absint($entry['user_id'] ?? 0)];
        }, array_values(array_filter($history, 'is_array')));
    }

    private function maybeMigrateLegacy(): void
    {
        if (get_option(Workspace::OPTION, null) !== null) return;
        $legacy = [];
        foreach (['h4bs_calendar_events', 'h4_calendar_events', 'barclay_calendar_events'] as $key) {
            $value = get_option($key, null);
            if ($value !== null) $legacy[$key] = $value;
        }
        $overrides = get_option('h4bs_calendar_overrides', []);
        $workspace = Workspace::defaults();
        foreach ($legacy as $value) if (is_array($value)) $workspace['source_events'] = array_merge($workspace['source_events'], $value);
        if (is_array($overrides)) $workspace['overrides'] = $overrides;
        if ($legacy || $overrides) $workspace['extensions']['legacy'] = ['option_keys' => array_keys($legacy), 'raw' => $legacy];
        update_option(Workspace::OPTION, Workspace::sanitize($workspace), false);
    }
}
