<?php
/**
 * Plugin activation: creates DB tables, seeds defaults, schedules cron.
 *
 * @package Brainwerk SEO Suite
 */

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

class Seoforge_Activator {

    public static function activate( $network_wide = false ) {
        self::create_tables();
        self::seed_defaults();
        self::schedule_events();
        self::mark_onboarding_pending();
        self::schedule_review_notice();
        self::flush_rewrites();
        self::maybe_prune_settings_overrides();
        self::maybe_clear_legacy_geo_cache();
    }

    /**
     * One-time (per network) cleanup: before 1.9.0, a subsite's first
     * settings save copied every network setting into its per-blog option
     * (see Seoforge_Settings::update()), silently shadowing that site from
     * future network-level changes. Runs prune_redundant_overrides() once,
     * gated by a flag so it doesn't re-walk every site on every subsequent
     * plugin update.
     */
    private static function maybe_prune_settings_overrides() {
        if ( ! is_multisite() ) {
            return;
        }
        if ( get_site_option( 'seoforge_settings_overrides_pruned' ) ) {
            return;
        }
        require_once SEOFORGE_PATH . 'includes/class-seoforge-settings.php';
        Seoforge_Settings::prune_redundant_overrides();
        update_site_option( 'seoforge_settings_overrides_pruned', 1 );
    }

    /**
     * One-time (per network) cleanup: before the geo country table shipped
     * uncompressed in the plugin itself, Seoforge_Geo::ensure_db() inflated
     * the bundled gz into wp-content/uploads/seoforge/geoip-country.bin on
     * first lookup. That runtime copy is now orphaned (the plugin reads its
     * own bundled data/geoip-country.dat directly) — remove the uploads
     * leftover, gated by a flag so this doesn't re-walk every site on every
     * subsequent plugin update. The filename here is fixed at the pre-1.10.3
     * uploads copy (`.bin`); 1.10.3 itself was never released with `.bin`
     * as the bundled name, so there is no second legacy name to clean up.
     */
    private static function maybe_clear_legacy_geo_cache() {
        if ( get_site_option( 'seoforge_geo_legacy_cache_cleared' ) ) {
            return;
        }
        if ( ! is_multisite() ) {
            self::delete_legacy_geo_cache_for_current_site();
        } else {
            $sites = get_sites( array( 'number' => 0, 'fields' => 'ids' ) );
            foreach ( $sites as $blog_id ) {
                switch_to_blog( (int) $blog_id );
                self::delete_legacy_geo_cache_for_current_site();
                restore_current_blog();
            }
        }
        update_site_option( 'seoforge_geo_legacy_cache_cleared', 1 );
    }

    private static function delete_legacy_geo_cache_for_current_site() {
        $upload = wp_upload_dir();
        $path   = trailingslashit( $upload['basedir'] ) . 'seoforge/geoip-country.bin';
        if ( file_exists( $path ) ) {
            wp_delete_file( $path );
        }
    }

    private static function schedule_review_notice() {
        require_once SEOFORGE_PATH . 'includes/class-seoforge-review-notice.php';
        Seoforge_Review_Notice::schedule_first_prompt();
    }

    private static function flush_rewrites() {
        // Sitemap rewrite rules need to be present right away so that
        // /seoforge-sitemap.xml works without a manual settings save.
        require_once SEOFORGE_PATH . 'includes/class-seoforge-sitemap.php';
        if ( ! method_exists( 'Seoforge_Sitemap', 'add_rewrite' ) ) {
            return;
        }
        if ( ! is_multisite() ) {
            Seoforge_Sitemap::add_rewrite();
            flush_rewrite_rules( false );
            return;
        }
        // Multisite — flush rewrites for every site so /seoforge-sitemap.xml
        // is recognized everywhere.
        $sites = get_sites( array( 'number' => 0, 'fields' => 'ids' ) );
        foreach ( $sites as $blog_id ) {
            switch_to_blog( (int) $blog_id );
            Seoforge_Sitemap::add_rewrite();
            flush_rewrite_rules( false );
            restore_current_blog();
        }
    }

    private static function mark_onboarding_pending() {
        // Only seed this on a genuinely fresh install. activate() also runs
        // from Seoforge::maybe_migrate() on every DB-version bump (i.e. on
        // every plugin update for every existing site) — without this guard
        // that would flip an already-completed onboarding back to "pending"
        // and re-show the wizard to sites that finished it long ago.
        if ( false === get_site_option( 'seoforge_onboarding_pending', false ) ) {
            update_site_option( 'seoforge_onboarding_pending', 1 );
        }
    }

    private static function create_tables() {
        global $wpdb;
        require_once ABSPATH . 'wp-admin/includes/upgrade.php';

        $charset_collate = $wpdb->get_charset_collate();
        $visits_table    = $wpdb->base_prefix . 'seoforge_visits';
        $sessions_table  = $wpdb->base_prefix . 'seoforge_sessions';
        $redirects_table = $wpdb->base_prefix . 'seoforge_redirects';
        $not_found_table = $wpdb->base_prefix . 'seoforge_404';
        $meta_table      = $wpdb->base_prefix . 'seoforge_meta';
        $daily_table     = $wpdb->base_prefix . 'seoforge_daily';
        $audit_table     = $wpdb->base_prefix . 'seoforge_audit';
        $history_table   = $wpdb->base_prefix . 'seoforge_history';

        // Hits / pageviews. One row per request.
        $sql_visits = "CREATE TABLE {$visits_table} (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            blog_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
            session_id CHAR(32) NOT NULL DEFAULT '',
            visitor_hash CHAR(40) NOT NULL DEFAULT '',
            post_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
            url VARCHAR(500) NOT NULL DEFAULT '',
            url_path VARCHAR(255) NOT NULL DEFAULT '',
            title VARCHAR(255) DEFAULT NULL,
            referrer_url VARCHAR(500) DEFAULT NULL,
            referrer_host VARCHAR(255) DEFAULT NULL,
            source_type VARCHAR(20) NOT NULL DEFAULT 'direct',
            source_name VARCHAR(60) DEFAULT NULL,
            search_term VARCHAR(255) DEFAULT NULL,
            utm_source VARCHAR(120) DEFAULT NULL,
            utm_medium VARCHAR(120) DEFAULT NULL,
            utm_campaign VARCHAR(120) DEFAULT NULL,
            utm_term VARCHAR(120) DEFAULT NULL,
            utm_content VARCHAR(120) DEFAULT NULL,
            ip_anon VARCHAR(45) DEFAULT NULL,
            country CHAR(2) DEFAULT NULL,
            region VARCHAR(80) DEFAULT NULL,
            city VARCHAR(120) DEFAULT NULL,
            user_agent VARCHAR(255) DEFAULT NULL,
            device_type VARCHAR(16) DEFAULT NULL,
            browser VARCHAR(40) DEFAULT NULL,
            os VARCHAR(40) DEFAULT NULL,
            is_bot TINYINT UNSIGNED NOT NULL DEFAULT 0,
            duration_sec INT UNSIGNED NOT NULL DEFAULT 0,
            scroll_depth TINYINT UNSIGNED NOT NULL DEFAULT 0,
            is_new_visitor TINYINT UNSIGNED NOT NULL DEFAULT 0,
            is_engaged TINYINT UNSIGNED NOT NULL DEFAULT 0,
            lcp_ms INT UNSIGNED DEFAULT NULL,
            cls_x1000 INT UNSIGNED DEFAULT NULL,
            inp_ms INT UNSIGNED DEFAULT NULL,
            status_code SMALLINT UNSIGNED NOT NULL DEFAULT 200,
            created_at DATETIME NOT NULL,
            PRIMARY KEY  (id),
            KEY idx_session (session_id),
            KEY idx_visitor (visitor_hash),
            KEY idx_post (post_id),
            KEY idx_blog_created (blog_id, created_at),
            KEY idx_source (source_type),
            KEY idx_path (url_path),
            KEY idx_bot (is_bot),
            KEY idx_country (country)
        ) {$charset_collate};";

        // Aggregated sessions (built by cron from visits).
        $sql_sessions = "CREATE TABLE {$sessions_table} (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            session_id CHAR(32) NOT NULL,
            visitor_hash CHAR(40) NOT NULL DEFAULT '',
            blog_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
            started_at DATETIME NOT NULL,
            ended_at DATETIME NOT NULL,
            pageviews INT UNSIGNED NOT NULL DEFAULT 1,
            entry_url VARCHAR(500) DEFAULT NULL,
            exit_url VARCHAR(500) DEFAULT NULL,
            source_type VARCHAR(20) NOT NULL DEFAULT 'direct',
            source_name VARCHAR(60) DEFAULT NULL,
            country CHAR(2) DEFAULT NULL,
            region VARCHAR(80) DEFAULT NULL,
            city VARCHAR(120) DEFAULT NULL,
            device_type VARCHAR(16) DEFAULT NULL,
            duration_sec INT UNSIGNED NOT NULL DEFAULT 0,
            is_bounce TINYINT UNSIGNED NOT NULL DEFAULT 0,
            PRIMARY KEY  (id),
            UNIQUE KEY uniq_session (session_id),
            KEY idx_started (started_at),
            KEY idx_blog (blog_id),
            KEY idx_visitor (visitor_hash)
        ) {$charset_collate};";

        // 301/302 manager.
        $sql_redirects = "CREATE TABLE {$redirects_table} (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            blog_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
            source_path VARCHAR(500) NOT NULL,
            target_url VARCHAR(500) NOT NULL,
            status_code SMALLINT UNSIGNED NOT NULL DEFAULT 301,
            match_type VARCHAR(16) NOT NULL DEFAULT 'exact',
            hits BIGINT UNSIGNED NOT NULL DEFAULT 0,
            last_hit_at DATETIME DEFAULT NULL,
            is_active TINYINT UNSIGNED NOT NULL DEFAULT 1,
            note VARCHAR(255) DEFAULT NULL,
            created_at DATETIME NOT NULL,
            PRIMARY KEY  (id),
            KEY idx_source (source_path(191)),
            KEY idx_blog_active (blog_id, is_active)
        ) {$charset_collate};";

        // 404 log (aggregated by URL).
        $sql_404 = "CREATE TABLE {$not_found_table} (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            blog_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
            url_path VARCHAR(500) NOT NULL,
            hits BIGINT UNSIGNED NOT NULL DEFAULT 0,
            last_referrer VARCHAR(500) DEFAULT NULL,
            last_user_agent VARCHAR(255) DEFAULT NULL,
            last_seen DATETIME NOT NULL,
            first_seen DATETIME NOT NULL,
            is_resolved TINYINT UNSIGNED NOT NULL DEFAULT 0,
            PRIMARY KEY  (id),
            UNIQUE KEY uniq_blog_path (blog_id, url_path(191)),
            KEY idx_last_seen (last_seen),
            KEY idx_resolved (is_resolved)
        ) {$charset_collate};";

        // Per-post SEO meta overrides.
        $sql_meta = "CREATE TABLE {$meta_table} (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            blog_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
            post_id BIGINT UNSIGNED NOT NULL,
            seo_title VARCHAR(255) DEFAULT NULL,
            seo_description VARCHAR(500) DEFAULT NULL,
            focus_keyword VARCHAR(120) DEFAULT NULL,
            og_title VARCHAR(255) DEFAULT NULL,
            og_description VARCHAR(500) DEFAULT NULL,
            og_image VARCHAR(500) DEFAULT NULL,
            twitter_card VARCHAR(40) DEFAULT NULL,
            canonical VARCHAR(500) DEFAULT NULL,
            robots_noindex TINYINT UNSIGNED NOT NULL DEFAULT 0,
            robots_nofollow TINYINT UNSIGNED NOT NULL DEFAULT 0,
            score TINYINT UNSIGNED NOT NULL DEFAULT 0,
            score_payload LONGTEXT,
            updated_at DATETIME NOT NULL,
            PRIMARY KEY  (id),
            UNIQUE KEY uniq_blog_post (blog_id, post_id),
            KEY idx_score (score)
        ) {$charset_collate};";

        // Pre-aggregated daily stats — built by hourly cron from visits.
        // Makes 30-day dashboard load O(30) instead of O(n) over millions of rows.
        $sql_daily = "CREATE TABLE {$daily_table} (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            blog_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
            day DATE NOT NULL,
            visits INT UNSIGNED NOT NULL DEFAULT 0,
            uniques INT UNSIGNED NOT NULL DEFAULT 0,
            sessions INT UNSIGNED NOT NULL DEFAULT 0,
            bounces INT UNSIGNED NOT NULL DEFAULT 0,
            duration_sum INT UNSIGNED NOT NULL DEFAULT 0,
            top_source VARCHAR(60) DEFAULT NULL,
            updated_at DATETIME NOT NULL,
            PRIMARY KEY  (id),
            UNIQUE KEY uniq_blog_day (blog_id, day),
            KEY idx_day (day)
        ) {$charset_collate};";

        // Settings/action audit trail — see class-seoforge-audit.php. summary is a
        // short human-readable string, deliberately not a serialized diff of the
        // changed values, to keep the PII footprint small.
        $sql_audit = "CREATE TABLE {$audit_table} (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            blog_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
            user_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
            user_login VARCHAR(60) NOT NULL DEFAULT '',
            action VARCHAR(60) NOT NULL,
            object_type VARCHAR(40) DEFAULT NULL,
            object_id VARCHAR(60) DEFAULT NULL,
            summary VARCHAR(255) NOT NULL DEFAULT '',
            created_at DATETIME NOT NULL,
            PRIMARY KEY  (id),
            KEY idx_blog_created (blog_id, created_at),
            KEY idx_action (action),
            KEY idx_user (user_id)
        ) {$charset_collate};";

        // Daily aggregate snapshot per blog — the "Eigenvergleich" (self-comparison)
        // history. One row per (blog_id, captured_on); no per-visitor data, so it can
        // be retained far longer than visits (see Seoforge_History::RETENTION_DEFAULT).
        // score_dist/check_fail_pct/hours_profile/weekdays_profile/ai_crawl_counts are
        // JSON — read rarely, and JSON is far easier to evolve than 9+ dedicated columns.
        // prev_hash/row_hash (1.9.1, DB v7) chain each row to the previous one for the
        // same blog_id — see Seoforge_History_Chain. NULL on a row that predates the
        // chain (migration does not fabricate hashes for pre-existing rows) or on any
        // install still on DB v6; both columns nullable so dbDelta's ALTER TABLE ADD
        // COLUMN never fails against rows that already exist.
        $sql_history = "CREATE TABLE {$history_table} (
            id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
            blog_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
            captured_on DATE NOT NULL,
            n_posts INT UNSIGNED NOT NULL DEFAULT 0,
            score_dist LONGTEXT,
            check_fail_pct LONGTEXT,
            hours_profile LONGTEXT,
            weekdays_profile LONGTEXT,
            pageviews INT UNSIGNED NOT NULL DEFAULT 0,
            sessions INT UNSIGNED NOT NULL DEFAULT 0,
            bounce_pct DECIMAL(5,2) NOT NULL DEFAULT 0,
            engaged_pct DECIMAL(5,2) NOT NULL DEFAULT 0,
            ai_crawl_counts LONGTEXT,
            ai_referrals INT UNSIGNED NOT NULL DEFAULT 0,
            ai_crawl_to_referral INT NOT NULL DEFAULT -1,
            prev_hash CHAR(64) DEFAULT NULL,
            row_hash CHAR(64) DEFAULT NULL,
            updated_at DATETIME NOT NULL,
            PRIMARY KEY  (id),
            UNIQUE KEY uniq_blog_day (blog_id, captured_on),
            KEY idx_captured_on (captured_on),
            KEY idx_row_hash (row_hash)
        ) {$charset_collate};";

        dbDelta( $sql_visits );
        dbDelta( $sql_sessions );
        dbDelta( $sql_redirects );
        dbDelta( $sql_404 );
        dbDelta( $sql_meta );
        dbDelta( $sql_daily );
        dbDelta( $sql_audit );
        dbDelta( $sql_history );

        update_site_option( SEOFORGE_DB_VERSION_KEY, SEOFORGE_DB_VERSION );
    }

    private static function seed_defaults() {
        $existing = get_site_option( SEOFORGE_OPTIONS_KEY );
        if ( $existing ) {
            return;
        }
        // Reinstall: if a settings backup survived a previous uninstall, restore
        // the user's configuration instead of seeding fresh defaults. Read the
        // option directly (no class dependency during activation).
        $backup = get_site_option( 'seoforge_settings_backup' ); // Seoforge_Settings::BACKUP_KEY
        if ( is_array( $backup ) && ! empty( $backup['settings'] ) && is_array( $backup['settings'] ) ) {
            update_site_option( SEOFORGE_OPTIONS_KEY, $backup['settings'] );
            if ( ! empty( $backup['ai'] ) && is_array( $backup['ai'] ) ) {
                update_site_option( 'seoforge_ai', $backup['ai'] );
            }
            return;
        }
        $defaults = array(
            // Tracking
            'tracking_enabled'           => true,
            'tracking_mode'              => 'server',     // server | beacon | hybrid
            'tracking_exclude_admins'    => true,
            'tracking_exclude_bots'      => true,
            'tracking_exclude_paths'     => array( '/wp-admin', '/wp-login.php', '/feed' ),
            'tracking_respect_dnt'       => true,
            'session_timeout_min'        => 30,

            // Privacy / GDPR
            'privacy_anonymize_ip'       => true,
            'privacy_visitor_hash_salt'  => wp_generate_password( 32, false, false ),
            'privacy_hash_rotate_daily'  => true,
            'privacy_store_full_url'     => true,
            'privacy_store_user_agent'   => true,

            // SEO
            'seo_meta_enabled'           => true,
            'seo_force_takeover'         => false,
            'seo_og_enabled'             => true,
            'seo_twitter_enabled'        => true,
            'seo_schema_enabled'         => true,
            'seo_schema_organization'    => array(
                'name' => get_bloginfo( 'name' ),
                'url'  => home_url( '/' ),
                'logo' => '',
            ),
            'seo_default_twitter_card'   => 'summary_large_image',
            'seo_breadcrumbs_enabled'    => true,
            'seo_canonical_enabled'      => true,
            'seo_remove_wp_emoji'        => true,
            'seo_clean_head'             => true, // remove generator, wlwmanifest etc.

            // Sitemap
            'sitemap_enabled'            => true,
            'sitemap_include_post_types' => array( 'post', 'page' ),
            'sitemap_include_taxonomies' => array( 'category', 'post_tag' ),
            'sitemap_max_per_chunk'      => 1000,

            // Robots
            'robots_custom_enabled'      => false,
            'robots_custom_content'      => "User-agent: *\nAllow: /\nDisallow: /wp-admin/\nDisallow: /?s=\n",

            // Redirects + 404
            'redirects_enabled'          => true,
            'log_404_enabled'            => true,
            'auto_redirect_404_attached' => false, // Pro

            // SEO-network sensor (benchmark hub) — on by default for fresh installs.
            'intel_enabled'               => true,

            // Notifications
            'notify_email_enabled'       => false,
            'notify_email_to'            => get_option( 'admin_email' ),
            'weekly_digest_enabled'      => false, // Consent-first: stays off until the onboarding checkbox opts in.
            'daily_digest_enabled'       => false,
            'traffic_spike_alerts'               => true,  // master on/off
            'traffic_spike_rule_percent_enabled' => true,  // Rule A: % over the learned baseline
            'traffic_spike_percent'              => 200,   // ...this many % above baseline
            'traffic_spike_rule_absolute_enabled' => false, // Rule B: absolute pageviews/hour
            'traffic_spike_absolute'             => 100,   // ...this many pageviews in one hour
            'traffic_spike_combine'              => 'or',  // 'or' = either rule, 'and' = both
            'traffic_spike_min'                  => 15,    // noise floor: ignore hours below this
            'traffic_spike_baseline_days'        => 28,    // learning window in days (>=3 weeks so each of the 168 weekday×hour slots warms up)

            // Retention
            'visit_retention_days'       => 365,
            'session_retention_days'     => 730,
            'log_404_retention_days'     => 90,
            'daily_retention_days'       => 730, // pre-aggregated rows are tiny; keep them roughly as long as sessions.
            'audit_retention_days'       => 180, // Seoforge_Audit — settings/action audit trail.
            'history_retention_days'     => 1095, // Seoforge_History — aggregate-only, no personal content, so it's kept far longer than visits (~3 years, ~1100 rows/site).

            // Data verification suite (1.9.1) — see Seoforge_Log_Reconciler / Seoforge_Verify.
            // Empty by default: on wp.org installs the nginx log path varies wildly and
            // www-data usually can't read /var/log/nginx/* anyway, so there is no safe
            // guess to seed here. An admin must explicitly point this at a path their
            // PHP-FPM user can read (or use the upload fallback in the Verify tab).
            'verify_nginx_log_path'      => '',
            'verify_nginx_log_format'    => 'combined', // combined | combined_xff

            // Misc
            // NOTE: 'delete_on_uninstall' is intentionally NOT seeded here. Its
            // absence means "undecided" — the onboarding wizard asks the user
            // explicitly, and uninstall.php treats an undecided state as "purge"
            // (data minimisation). An explicit user choice (0 or 1) is honoured.
            'persist_settings'           => true, // keep a backup so a reinstall restores config
            'plugin_version'             => SEOFORGE_VERSION,
        );
        update_site_option( SEOFORGE_OPTIONS_KEY, $defaults );
    }

    private static function schedule_events() {
        if ( ! is_multisite() ) {
            self::schedule_events_for_current_site();
            return;
        }
        // WP-Cron events live in a per-site option, but activate() only runs
        // once even for a network-wide activation — without this loop, every
        // subsite except the one WP happened to call activate() on would
        // never get seoforge_cleanup_visits/seoforge_aggregate_sessions
        // scheduled at all (unlike the digest crons, which self-heal via
        // Seoforge_Notifications::maybe_schedule() on 'init'). Mirrors the
        // same per-site loop flush_rewrites() already uses below.
        $sites = get_sites( array( 'number' => 0, 'fields' => 'ids' ) );
        foreach ( $sites as $blog_id ) {
            switch_to_blog( (int) $blog_id );
            self::schedule_events_for_current_site();
            restore_current_blog();
        }
    }

    private static function schedule_events_for_current_site() {
        if ( ! wp_next_scheduled( 'seoforge_cleanup_visits' ) ) {
            wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'seoforge_cleanup_visits' );
        }
        if ( ! wp_next_scheduled( 'seoforge_aggregate_sessions' ) ) {
            wp_schedule_event( time() + ( 5 * MINUTE_IN_SECONDS ), 'hourly', 'seoforge_aggregate_sessions' );
        }
        // Independent of the intel network opt-in (Seoforge_Intel::CRON_HOOK) — the
        // self-comparison history works for opted-out sites too, that's the point.
        // Runs before the 03:10 UTC intel push so both read "yesterday" consistently.
        if ( ! wp_next_scheduled( 'seoforge_history_capture' ) ) {
            wp_schedule_event( strtotime( 'tomorrow 02:40 UTC' ), 'daily', 'seoforge_history_capture' );
        }
        // After history capture (02:40) and the intel push (03:10) so a gap/chain
        // check run today already sees today's just-captured row. Self-heals too
        // (Seoforge_Verify::maybe_schedule() on init) — see that class's docblock.
        if ( ! wp_next_scheduled( 'seoforge_verify_daily' ) ) {
            wp_schedule_event( strtotime( 'tomorrow 03:30 UTC' ), 'daily', 'seoforge_verify_daily' );
        }
        // Explicit UTC — matches how every timestamp in the DB (created_at,
        // started_at, day, …) is stored (gmdate()/current_time('mysql',1)).
        // Without the suffix, strtotime() resolves against PHP's default
        // timezone, which need not be UTC and need not match the site's
        // configured WP timezone either — an ambiguous third clock.
        if ( ! wp_next_scheduled( 'seoforge_weekly_digest' ) ) {
            wp_schedule_event( strtotime( 'next monday 07:00 UTC' ), 'weekly', 'seoforge_weekly_digest' );
        }
        if ( ! wp_next_scheduled( 'seoforge_daily_digest' ) ) {
            wp_schedule_event( strtotime( 'tomorrow 07:00 UTC' ), 'daily', 'seoforge_daily_digest' );
        }
    }
}
