<?php

namespace WPSN\Admin;

use WPSN\Modules\HeadCleanup\HeadCleanupProfile;
use WPSN\Modules\Licensing\PlanMatrix;
use WPSN\SEO\SchemaCustomJsonLdResolver;

defined('ABSPATH') || exit;

final class SettingsRepository
{
    public const OPTION_NAME = 'wpsn_settings';

    /**
     * @return array<string, mixed>
     */
    public function getAll(): array
    {
        $value = get_option(self::OPTION_NAME);
        if (!is_array($value)) {
            $value = [];
        }

        return array_replace_recursive($this->defaults(), $value);
    }

    /**
     * @param array<string, mixed> $defaults
     */
    public function reset(array $defaults = []): void
    {
        update_option(self::OPTION_NAME, array_replace_recursive($this->defaults(), $defaults));
    }

    /**
     * @param array<string, mixed> $settings
     */
    public function update(array $settings): void
    {
        $prev = $this->getAll();
        $settings = $this->sanitize($settings);

        // Prompt template versioning (bump when template text changes).
        $prevTitleTpl = (string) ($prev['ai']['prompts']['title_template'] ?? '');
        $prevDescTpl  = (string) ($prev['ai']['prompts']['description_template'] ?? '');
        $prevAltTpl   = (string) ($prev['ai']['prompts']['alt_template'] ?? '');
        $prevBriefTpl = (string) ($prev['ai']['prompts']['brief_template'] ?? '');
        $prevSerpGapTpl = (string) ($prev['ai']['prompts']['serp_gap_template'] ?? '');
        $prevSerpGapFaqTpl = (string) ($prev['ai']['prompts']['serp_gap_faq_template'] ?? '');

        $prevTitleVer = (int) ($prev['ai']['prompts']['title_version'] ?? 1);
        $prevDescVer  = (int) ($prev['ai']['prompts']['description_version'] ?? 1);
        $prevAltVer   = (int) ($prev['ai']['prompts']['alt_version'] ?? 1);
        $prevBriefVer = (int) ($prev['ai']['prompts']['brief_version'] ?? 1);
        $prevSerpGapVer = (int) ($prev['ai']['prompts']['serp_gap_version'] ?? 1);
        $prevSerpGapFaqVer = (int) ($prev['ai']['prompts']['serp_gap_faq_version'] ?? 1);

        $newTitleTpl = (string) ($settings['ai']['prompts']['title_template'] ?? '');
        $newDescTpl  = (string) ($settings['ai']['prompts']['description_template'] ?? '');
        $newAltTpl   = (string) ($settings['ai']['prompts']['alt_template'] ?? '');
        $newBriefTpl = (string) ($settings['ai']['prompts']['brief_template'] ?? '');
        $newSerpGapTpl = (string) ($settings['ai']['prompts']['serp_gap_template'] ?? '');
        $newSerpGapFaqTpl = (string) ($settings['ai']['prompts']['serp_gap_faq_template'] ?? '');

        if (trim($newTitleTpl) !== trim($prevTitleTpl)) {
            $settings['ai']['prompts']['title_version'] = max(1, $prevTitleVer + 1);
        } else {
            $settings['ai']['prompts']['title_version'] = max(1, $prevTitleVer);
        }

        if (trim($newDescTpl) !== trim($prevDescTpl)) {
            $settings['ai']['prompts']['description_version'] = max(1, $prevDescVer + 1);
        } else {
            $settings['ai']['prompts']['description_version'] = max(1, $prevDescVer);
        }

        if (trim($newAltTpl) !== trim($prevAltTpl)) {
            $settings['ai']['prompts']['alt_version'] = max(1, $prevAltVer + 1);
        } else {
            $settings['ai']['prompts']['alt_version'] = max(1, $prevAltVer);
        }

        if (trim($newBriefTpl) !== trim($prevBriefTpl)) {
            $settings['ai']['prompts']['brief_version'] = max(1, $prevBriefVer + 1);
        } else {
            $settings['ai']['prompts']['brief_version'] = max(1, $prevBriefVer);
        }

        if (trim($newSerpGapTpl) !== trim($prevSerpGapTpl)) {
            $settings['ai']['prompts']['serp_gap_version'] = max(1, $prevSerpGapVer + 1);
        } else {
            $settings['ai']['prompts']['serp_gap_version'] = max(1, $prevSerpGapVer);
        }

        if (trim($newSerpGapFaqTpl) !== trim($prevSerpGapFaqTpl)) {
            $settings['ai']['prompts']['serp_gap_faq_version'] = max(1, $prevSerpGapFaqVer + 1);
        } else {
            $settings['ai']['prompts']['serp_gap_faq_version'] = max(1, $prevSerpGapFaqVer);
        }

        update_option(self::OPTION_NAME, $settings);
    }

    /**
     * @param array<string, mixed> $input
     * @return array<string, mixed>
     */
    public function sanitize(array $input): array
    {
        $out = $this->defaults();

        $out['debug'] = !empty($input['debug']);

        $out['separator'] = isset($input['separator'])
            ? sanitize_text_field((string) $input['separator'])
            : $out['separator'];

        // UI mode (Simple / Pro). Only affects UI visibility; does not change SEO output.
        if (isset($input['ui']) && is_array($input['ui'])) {
            $mode = isset($input['ui']['mode']) ? sanitize_key((string) $input['ui']['mode']) : '';
            if (!in_array($mode, ['simple', 'pro'], true)) {
                $mode = $out['ui']['mode'];
            }
            $out['ui']['mode'] = $mode;
        }

        if (isset($input['templates']) && is_array($input['templates'])) {
            $out['templates']['title'] = isset($input['templates']['title'])
                ? sanitize_text_field((string) $input['templates']['title'])
                : $out['templates']['title'];

            // description can be a bit longer; still sanitize to plain text.
            $out['templates']['description'] = isset($input['templates']['description'])
                ? sanitize_text_field((string) $input['templates']['description'])
                : $out['templates']['description'];

            // Author archives (author pages).
            $out['templates']['author_title'] = isset($input['templates']['author_title'])
                ? sanitize_text_field((string) $input['templates']['author_title'])
                : $out['templates']['author_title'];

            $out['templates']['author_description'] = isset($input['templates']['author_description'])
                ? sanitize_text_field((string) $input['templates']['author_description'])
                : $out['templates']['author_description'];
        }

        // Site verification meta tags (Search Console / Bing / Yandex / etc.).
        if (isset($input['verification']) && is_array($input['verification'])) {
            $ver = $input['verification'];

            $out['verification']['google'] = isset($ver['google'])
                ? sanitize_text_field((string) $ver['google'])
                : $out['verification']['google'];

            // Bing / Microsoft Webmaster Tools.
            $out['verification']['bing'] = isset($ver['bing'])
                ? sanitize_text_field((string) $ver['bing'])
                : $out['verification']['bing'];

            $out['verification']['yandex'] = isset($ver['yandex'])
                ? sanitize_text_field((string) $ver['yandex'])
                : $out['verification']['yandex'];

            $out['verification']['baidu'] = isset($ver['baidu'])
                ? sanitize_text_field((string) $ver['baidu'])
                : $out['verification']['baidu'];

            $out['verification']['ahrefs'] = isset($ver['ahrefs'])
                ? sanitize_text_field((string) $ver['ahrefs'])
                : $out['verification']['ahrefs'];
        }

        // Robots (meta robots defaults).
        if (isset($input['robots']) && is_array($input['robots'])) {
            // Context rules.
            if (isset($input['robots']['contexts']) && is_array($input['robots']['contexts'])) {
                foreach (['home', 'singular', 'term', 'archive', 'search', '404'] as $ctx) {
                    $ctxIn = $input['robots']['contexts'][$ctx] ?? [];
                    if (!is_array($ctxIn)) {
                        $ctxIn = [];
                    }
                    $out['robots']['contexts'][$ctx]['index']  = !empty($ctxIn['index']);
                    $out['robots']['contexts'][$ctx]['follow'] = !empty($ctxIn['follow']);
                }
            }

            // Optional: noindex paginated archives/terms.
            $out['robots']['noindex_paged'] = !empty($input['robots']['noindex_paged']);

            // Advanced directives.
            if (isset($input['robots']['advanced']) && is_array($input['robots']['advanced'])) {
                $adv = $input['robots']['advanced'];

                $mip = isset($adv['max-image-preview']) ? sanitize_text_field((string) $adv['max-image-preview']) : '';
                $allowedMip = ['', 'large', 'standard', 'none'];
                $out['robots']['advanced']['max-image-preview'] = in_array($mip, $allowedMip, true) ? $mip : $out['robots']['advanced']['max-image-preview'];

                $ms = isset($adv['max-snippet']) ? trim((string) $adv['max-snippet']) : '';
                if ($ms !== '' && is_numeric($ms)) {
                    $out['robots']['advanced']['max-snippet'] = (string) (int) $ms;
                }

                $mvp = isset($adv['max-video-preview']) ? trim((string) $adv['max-video-preview']) : '';
                if ($mvp !== '' && is_numeric($mvp)) {
                    $out['robots']['advanced']['max-video-preview'] = (string) (int) $mvp;
                }

                $out['robots']['advanced']['noarchive'] = !empty($adv['noarchive']);
                $out['robots']['advanced']['nosnippet'] = !empty($adv['nosnippet']);
            }

            // URL pattern-based noindex rules (applied via wp_robots).
            if (isset($input['robots']['url_noindex_rules']) && is_array($input['robots']['url_noindex_rules'])) {
                $out['robots']['url_noindex_rules'] = [];

                // URL pattern matchers used by Crawl Budget + Site Crawl “one-click” actions.
                // query_param_int_gt1 is a safer matcher for pagination-like params (e.g. ?paged=2),
                // avoiding accidental noindex on ?paged=1.
                $allowedMatch = ['path_prefix', 'path_regex', 'query_param', 'query_param_prefix', 'query_param_int_gt1'];
                $count = 0;

                foreach ($input['robots']['url_noindex_rules'] as $r) {
                    if (!is_array($r)) {
                        continue;
                    }

                    $id = isset($r['id']) ? sanitize_key((string) $r['id']) : '';
                    $enabled = !empty($r['enabled']) ? 1 : 0;
                    $match = isset($r['match']) ? sanitize_key((string) $r['match']) : '';
                    $pattern = isset($r['pattern']) ? trim((string) $r['pattern']) : '';
                    $pathPrefix = isset($r['path_prefix']) ? trim((string) $r['path_prefix']) : '';
                    $note = isset($r['note']) ? sanitize_text_field((string) $r['note']) : '';
                    $source = isset($r['source']) ? sanitize_key((string) $r['source']) : '';
                    $createdAt = isset($r['created_at']) ? sanitize_text_field((string) $r['created_at']) : '';

                    if ($id === '' || $pattern === '' || !in_array($match, $allowedMatch, true)) {
                        continue;
                    }

                    // Allow regex tokens, but keep it single-line and safe.
                    $pattern = sanitize_text_field($pattern);
                    $pathPrefix = $pathPrefix !== '' ? sanitize_text_field($pathPrefix) : '';
                    if ($pathPrefix !== '' && $pathPrefix[0] !== '/') {
                        $pathPrefix = '/' . ltrim($pathPrefix, '/');
                    }

                    $out['robots']['url_noindex_rules'][] = [
                        'id' => $id,
                        'enabled' => $enabled,
                        'match' => $match,
                        'pattern' => $pattern,
                        'path_prefix' => $pathPrefix,
                        'note' => $note,
                        'source' => $source,
                        'created_at' => $createdAt,
                    ];

                    $count++;
                    if ($count >= 500) {
                        break;
                    }
                }
            }
        }

        // Archive controls (specific archive robots/disable modes + attachment pages).
        if (isset($input['archive_controls']) && is_array($input['archive_controls'])) {
            $allowedArchiveModes = ['index', 'noindex', 'disable_301_to_home', 'disable_301_to_custom', 'disable_404'];
            foreach (['author', 'date', 'post_type'] as $bucket) {
                $rule = $input['archive_controls'][$bucket] ?? [];
                if (!is_array($rule)) {
                    $rule = [];
                }

                $legacyIndex = !array_key_exists('index', $rule) || !empty($rule['index']);
                $mode = isset($rule['mode']) ? sanitize_key((string) $rule['mode']) : '';
                if (!in_array($mode, $allowedArchiveModes, true)) {
                    $mode = $legacyIndex ? 'index' : 'noindex';
                }

                $out['archive_controls'][$bucket]['enabled'] = !empty($rule['enabled']);
                $out['archive_controls'][$bucket]['mode'] = $mode;
                $out['archive_controls'][$bucket]['index'] = ($mode === 'index');
                $out['archive_controls'][$bucket]['follow'] = !empty($rule['follow']);
                $out['archive_controls'][$bucket]['redirect_url'] = isset($rule['redirect_url']) ? esc_url_raw(trim((string) $rule['redirect_url'])) : '';
            }

            $attachments = $input['archive_controls']['attachments'] ?? [];
            if (!is_array($attachments)) {
                $attachments = [];
            }

            $mode = isset($attachments['mode']) ? sanitize_key((string) $attachments['mode']) : '';
            $allowedModes = ['default', 'noindex', 'redirect_file', 'redirect_parent'];
            $out['archive_controls']['attachments']['mode'] = in_array($mode, $allowedModes, true)
                ? $mode
                : $out['archive_controls']['attachments']['mode'];
        }

        // Schema
        if (isset($input['schema']) && is_array($input['schema'])) {
            $schema = $input['schema'];

            $out['schema']['org_name'] = isset($schema['org_name'])
                ? sanitize_text_field((string) $schema['org_name'])
                : $out['schema']['org_name'];

            $out['schema']['org_alternate_name'] = isset($schema['org_alternate_name'])
                ? sanitize_text_field((string) $schema['org_alternate_name'])
                : $out['schema']['org_alternate_name'];

            $out['schema']['org_description'] = isset($schema['org_description'])
                ? sanitize_textarea_field((string) $schema['org_description'])
                : $out['schema']['org_description'];

            $out['schema']['org_email'] = isset($schema['org_email'])
                ? sanitize_email((string) $schema['org_email'])
                : $out['schema']['org_email'];

            $out['schema']['org_telephone'] = isset($schema['org_telephone'])
                ? sanitize_text_field((string) $schema['org_telephone'])
                : $out['schema']['org_telephone'];

            $out['schema']['org_legal_name'] = isset($schema['org_legal_name'])
                ? sanitize_text_field((string) $schema['org_legal_name'])
                : $out['schema']['org_legal_name'];

            $out['schema']['org_founding_date'] = isset($schema['org_founding_date'])
                ? sanitize_text_field((string) $schema['org_founding_date'])
                : $out['schema']['org_founding_date'];

            $out['schema']['org_number_employees'] = isset($schema['org_number_employees'])
                ? max(0, (int) $schema['org_number_employees'])
                : $out['schema']['org_number_employees'];

            $out['schema']['org_vat_id'] = isset($schema['org_vat_id'])
                ? sanitize_text_field((string) $schema['org_vat_id'])
                : $out['schema']['org_vat_id'];

            $out['schema']['org_tax_id'] = isset($schema['org_tax_id'])
                ? sanitize_text_field((string) $schema['org_tax_id'])
                : $out['schema']['org_tax_id'];

            $out['schema']['org_iso6523_code'] = isset($schema['org_iso6523_code'])
                ? sanitize_text_field((string) $schema['org_iso6523_code'])
                : $out['schema']['org_iso6523_code'];

            $out['schema']['org_duns'] = isset($schema['org_duns'])
                ? sanitize_text_field((string) $schema['org_duns'])
                : $out['schema']['org_duns'];

            $out['schema']['org_lei_code'] = isset($schema['org_lei_code'])
                ? sanitize_text_field((string) $schema['org_lei_code'])
                : $out['schema']['org_lei_code'];

            $out['schema']['org_naics'] = isset($schema['org_naics'])
                ? sanitize_text_field((string) $schema['org_naics'])
                : $out['schema']['org_naics'];

            $out['schema']['org_logo_url'] = isset($schema['org_logo_url'])
                ? esc_url_raw((string) $schema['org_logo_url'])
                : $out['schema']['org_logo_url'];

            // One URL per line.
            $sameAs = isset($schema['org_same_as']) ? (string) $schema['org_same_as'] : '';
            $urls = [];
            if ($sameAs !== '') {
                $lines = preg_split('/\r\n|\r|\n/', $sameAs) ?: [];
                foreach ($lines as $line) {
                    $line = trim((string) $line);
                    if ($line === '') {
                        continue;
                    }
                    $u = esc_url_raw($line);
                    if ($u !== '') {
                        $urls[] = $u;
                    }
                }
            }
            $out['schema']['org_same_as'] = array_values(array_unique($urls));

            $policyUrls = isset($schema['policy_urls']) && is_array($schema['policy_urls'])
                ? $schema['policy_urls']
                : [];
            foreach (array_keys($out['schema']['policy_urls']) as $policyKey) {
                $raw = isset($policyUrls[$policyKey]) ? trim((string) $policyUrls[$policyKey]) : '';
                if ($raw !== '' && $raw[0] === '/') {
                    $raw = home_url($raw);
                }
                $out['schema']['policy_urls'][$policyKey] = esc_url_raw($raw);
            }

            $defaultTypes = isset($schema['default_types']) && is_array($schema['default_types'])
                ? $schema['default_types']
                : [];
            $cleanDefaultTypes = [];
            foreach ($defaultTypes as $postType => $rawType) {
                $postType = sanitize_key((string) $postType);
                if ($postType === '' || $postType === 'attachment') {
                    continue;
                }

                $type = sanitize_key((string) $rawType);
                if (!in_array($type, ['', 'default', 'none', 'article', 'webpage', 'product'], true)) {
                    continue;
                }

                if ($type === '' || $type === 'default') {
                    continue;
                }

                $cleanDefaultTypes[$postType] = $type;
            }
            $out['schema']['default_types'] = $cleanDefaultTypes;

            if (isset($schema['custom_json_ld']) && is_array($schema['custom_json_ld'])) {
                $custom = $schema['custom_json_ld'];
                $templates = isset($custom['post_type_templates']) && is_array($custom['post_type_templates'])
                    ? $custom['post_type_templates']
                    : [];

                $sanitizedTemplates = [];
                foreach ($templates as $postType => $raw) {
                    $postType = sanitize_key((string) $postType);
                    if ($postType === '' || $postType === 'attachment') {
                        continue;
                    }

                    $value = SchemaCustomJsonLdResolver::sanitizeRaw(is_string($raw) ? $raw : '');
                    if ($value === '') {
                        continue;
                    }

                    $sanitizedTemplates[$postType] = $value;
                }

                $out['schema']['custom_json_ld']['post_type_templates'] = $sanitizedTemplates;
            }
        }

        // Social
        if (isset($input['social']) && is_array($input['social'])) {
            $soc = $input['social'];
            $url = isset($soc['default_image_url']) ? trim((string) $soc['default_image_url']) : '';
            if ($url !== '') {
                // Allow site-relative.
                if (strpos($url, '/') === 0 && strpos($url, '//') !== 0) {
                    $url = home_url($url);
                }
                $url = esc_url_raw($url);
            }
            $out['social']['default_image_url'] = is_string($url) ? $url : '';

            $out['social']['fallback_author_image'] = !empty($soc['fallback_author_image']);
        }

        // Licensing (optional)
        if (isset($input['licensing']) && is_array($input['licensing'])) {
            $lic = $input['licensing'];

            $out['licensing']['server_url'] = isset($lic['server_url'])
                ? esc_url_raw((string) $lic['server_url'])
                : $out['licensing']['server_url'];

            $out['licensing']['verify_path'] = isset($lic['verify_path'])
                ? sanitize_text_field((string) $lic['verify_path'])
                : $out['licensing']['verify_path'];

            $out['licensing']['public_key_pem'] = isset($lic['public_key_pem'])
                ? sanitize_textarea_field((string) $lic['public_key_pem'])
                : $out['licensing']['public_key_pem'];

            $out['licensing']['update_server_url'] = isset($lic['update_server_url'])
                ? esc_url_raw((string) $lic['update_server_url'])
                : $out['licensing']['update_server_url'];

            $out['licensing']['update_path'] = isset($lic['update_path'])
                ? sanitize_text_field((string) $lic['update_path'])
                : $out['licensing']['update_path'];

            $out['licensing']['enable_updates'] = !empty($lic['enable_updates']);

            $out['licensing']['purchase_url'] = isset($lic['purchase_url'])
                ? $this->sanitizeMaybeRelativeUrl((string) $lic['purchase_url'])
                : $out['licensing']['purchase_url'];

            $out['licensing']['billing_portal_url'] = isset($lic['billing_portal_url'])
                ? $this->sanitizeMaybeRelativeUrl((string) $lic['billing_portal_url'])
                : $out['licensing']['billing_portal_url'];

            $out['licensing']['account_portal_url'] = isset($lic['account_portal_url'])
                ? $this->sanitizeMaybeRelativeUrl((string) $lic['account_portal_url'])
                : $out['licensing']['account_portal_url'];

            $out['licensing']['invoices_url'] = isset($lic['invoices_url'])
                ? $this->sanitizeMaybeRelativeUrl((string) $lic['invoices_url'])
                : $out['licensing']['invoices_url'];

            $out['licensing']['change_plan_url'] = isset($lic['change_plan_url'])
                ? $this->sanitizeMaybeRelativeUrl((string) $lic['change_plan_url'])
                : $out['licensing']['change_plan_url'];

            $out['licensing']['cancel_url'] = isset($lic['cancel_url'])
                ? $this->sanitizeMaybeRelativeUrl((string) $lic['cancel_url'])
                : $out['licensing']['cancel_url'];

            $out['licensing']['payment_method_url'] = isset($lic['payment_method_url'])
                ? $this->sanitizeMaybeRelativeUrl((string) $lic['payment_method_url'])
                : $out['licensing']['payment_method_url'];

            $out['licensing']['billing_support_url'] = isset($lic['billing_support_url'])
                ? $this->sanitizeMaybeRelativeUrl((string) $lic['billing_support_url'])
                : $out['licensing']['billing_support_url'];

            $out['licensing']['billing_embed'] = !empty($lic['billing_embed']);

            $embedHeight = isset($lic['billing_embed_height']) ? (int) $lic['billing_embed_height'] : (int) $out['licensing']['billing_embed_height'];
            $out['licensing']['billing_embed_height'] = max(480, min(2000, $embedHeight));

            $allowHosts = $lic['billing_embed_allowed_hosts'] ?? $out['licensing']['billing_embed_allowed_hosts'];
            if (is_string($allowHosts)) {
                $allowHosts = preg_split('/[\r\n,]+/', $allowHosts) ?: [];
            }
            if (!is_array($allowHosts)) {
                $allowHosts = [];
            }
            $sanitizedHosts = [];
            foreach ($allowHosts as $host) {
                if (!is_scalar($host)) {
                    continue;
                }
                $host = strtolower(trim((string) $host));
                if ($host === '') {
                    continue;
                }
                $parsedHost = wp_parse_url($host, PHP_URL_HOST);
                if (is_string($parsedHost) && $parsedHost !== '') {
                    $host = strtolower($parsedHost);
                }
                if (!preg_match('/^[a-z0-9.-]+$/', $host)) {
                    continue;
                }
                $sanitizedHosts[] = $host;
            }
            $out['licensing']['billing_embed_allowed_hosts'] = array_values(array_unique($sanitizedHosts));

            $out['licensing']['billing_sync_enabled'] = !empty($lic['billing_sync_enabled']);

            $out['licensing']['billing_sync_path'] = isset($lic['billing_sync_path'])
                ? sanitize_text_field((string) $lic['billing_sync_path'])
                : $out['licensing']['billing_sync_path'];

            $billingSyncTimeout = isset($lic['billing_sync_timeout']) ? (int) $lic['billing_sync_timeout'] : (int) $out['licensing']['billing_sync_timeout'];
            $out['licensing']['billing_sync_timeout'] = max(3, min(60, $billingSyncTimeout));

            $billingSyncTtl = isset($lic['billing_sync_cache_ttl_hours']) ? (int) $lic['billing_sync_cache_ttl_hours'] : (int) $out['licensing']['billing_sync_cache_ttl_hours'];
            $out['licensing']['billing_sync_cache_ttl_hours'] = max(1, min(168, $billingSyncTtl));

            $out['licensing']['billing_sync_scheduled'] = !empty($lic['billing_sync_scheduled']);
            $out['licensing']['billing_sync_require_signed'] = !empty($lic['billing_sync_require_signed']);

            $out['licensing']['self_serve_enabled'] = !empty($lic['self_serve_enabled']);

            $out['licensing']['self_serve_catalog_path'] = isset($lic['self_serve_catalog_path'])
                ? sanitize_text_field((string) $lic['self_serve_catalog_path'])
                : $out['licensing']['self_serve_catalog_path'];

            $out['licensing']['self_serve_checkout_path'] = isset($lic['self_serve_checkout_path'])
                ? sanitize_text_field((string) $lic['self_serve_checkout_path'])
                : $out['licensing']['self_serve_checkout_path'];

            $out['licensing']['self_serve_action_path'] = isset($lic['self_serve_action_path'])
                ? sanitize_text_field((string) $lic['self_serve_action_path'])
                : $out['licensing']['self_serve_action_path'];

            $out['licensing']['self_serve_manage_path'] = isset($lic['self_serve_manage_path'])
                ? sanitize_text_field((string) $lic['self_serve_manage_path'])
                : $out['licensing']['self_serve_manage_path'];

            $selfServeTimeout = isset($lic['self_serve_timeout']) ? (int) $lic['self_serve_timeout'] : (int) $out['licensing']['self_serve_timeout'];
            $out['licensing']['self_serve_timeout'] = max(3, min(60, $selfServeTimeout));

            $selfServeTtl = isset($lic['self_serve_cache_ttl_hours']) ? (int) $lic['self_serve_cache_ttl_hours'] : (int) $out['licensing']['self_serve_cache_ttl_hours'];
            $out['licensing']['self_serve_cache_ttl_hours'] = max(1, min(168, $selfServeTtl));

            $out['licensing']['self_serve_require_signed'] = !empty($lic['self_serve_require_signed']);

            $ttl = isset($lic['cache_ttl_hours']) ? (int) $lic['cache_ttl_hours'] : (int) $out['licensing']['cache_ttl_hours'];
            $out['licensing']['cache_ttl_hours'] = max(1, min(168, $ttl));

            $grace = isset($lic['grace_days']) ? (int) $lic['grace_days'] : (int) $out['licensing']['grace_days'];
            $out['licensing']['grace_days'] = max(0, min(60, $grace));

            $out['licensing']['enforce_entitlements'] = !empty($lic['enforce_entitlements']);
            $out['licensing']['require_signed'] = !empty($lic['require_signed']);
            $out['licensing']['scheduled_verify'] = !empty($lic['scheduled_verify']);
        }

        // SaaS Sync (optional)
        if (isset($input['saas_sync']) && is_array($input['saas_sync'])) {
            $ss = $input['saas_sync'];

            $out['saas_sync']['enabled'] = !empty($ss['enabled']);

            $out['saas_sync']['server_url'] = isset($ss['server_url'])
                ? esc_url_raw((string) $ss['server_url'])
                : $out['saas_sync']['server_url'];

            $out['saas_sync']['sync_path'] = isset($ss['sync_path'])
                ? sanitize_text_field((string) $ss['sync_path'])
                : $out['saas_sync']['sync_path'];

            $ttl = isset($ss['cache_ttl_hours']) ? (int) $ss['cache_ttl_hours'] : (int) $out['saas_sync']['cache_ttl_hours'];
            $out['saas_sync']['cache_ttl_hours'] = max(1, min(168, $ttl));

            $grace = isset($ss['grace_days']) ? (int) $ss['grace_days'] : (int) $out['saas_sync']['grace_days'];
            $out['saas_sync']['grace_days'] = max(0, min(60, $grace));

            $out['saas_sync']['require_signed'] = !empty($ss['require_signed']);
            $out['saas_sync']['scheduled_sync'] = !empty($ss['scheduled_sync']);

            $schedule = isset($ss['schedule']) ? sanitize_key((string) $ss['schedule']) : (string) $out['saas_sync']['schedule'];
            $allowedSchedules = ['hourly', 'twicedaily', 'daily', 'wpsn_15min', 'wpsn_10min', 'wpsn_5min'];
            $out['saas_sync']['schedule'] = in_array($schedule, $allowedSchedules, true) ? $schedule : (string) $out['saas_sync']['schedule'];

            $out['saas_sync']['enforce_remote_toggles'] = !empty($ss['enforce_remote_toggles']);
            $out['saas_sync']['enforce_serp_providers'] = !empty($ss['enforce_serp_providers']);

            $timeout = isset($ss['timeout']) ? (int) $ss['timeout'] : (int) $out['saas_sync']['timeout'];
            $out['saas_sync']['timeout'] = max(3, min(60, $timeout));

            $serpPath = isset($ss['serp_path']) ? sanitize_text_field((string) $ss['serp_path']) : (string) $out['saas_sync']['serp_path'];
            $out['saas_sync']['serp_path'] = $serpPath !== '' ? $serpPath : (string) $out['saas_sync']['serp_path'];

            $serpTimeout = isset($ss['serp_timeout']) ? (int) $ss['serp_timeout'] : (int) $out['saas_sync']['serp_timeout'];
            $out['saas_sync']['serp_timeout'] = max(3, min(60, $serpTimeout));

            // Optional: competitor page fetch endpoint used by SERP Gap deep mode.
            $serpPagePath = isset($ss['serp_page_path'])
                ? sanitize_text_field((string) $ss['serp_page_path'])
                : (string) ($out['saas_sync']['serp_page_path'] ?? '/wp-seo-next/serp/page');
            $out['saas_sync']['serp_page_path'] = $serpPagePath !== ''
                ? $serpPagePath
                : (string) ($out['saas_sync']['serp_page_path'] ?? '/wp-seo-next/serp/page');

            $serpPageTimeout = isset($ss['serp_page_timeout']) ? (int) $ss['serp_page_timeout'] : (int) ($out['saas_sync']['serp_page_timeout'] ?? 20);
            $out['saas_sync']['serp_page_timeout'] = max(3, min(60, $serpPageTimeout));

            // Optional: AI proxy endpoint used when ai.external.provider = saas_proxy.
            $aiPath = isset($ss['ai_path'])
                ? sanitize_text_field((string) $ss['ai_path'])
                : (string) ($out['saas_sync']['ai_path'] ?? '/wp-seo-next/ai/generate');
            $out['saas_sync']['ai_path'] = $aiPath !== ''
                ? $aiPath
                : (string) ($out['saas_sync']['ai_path'] ?? '/wp-seo-next/ai/generate');

            $aiTimeout = isset($ss['ai_timeout']) ? (int) $ss['ai_timeout'] : (int) ($out['saas_sync']['ai_timeout'] ?? 60);
            $out['saas_sync']['ai_timeout'] = max(3, min(120, $aiTimeout));

            $aiCreditsPath = isset($ss['ai_credits_path'])
                ? sanitize_text_field((string) $ss['ai_credits_path'])
                : (string) ($out['saas_sync']['ai_credits_path'] ?? '/wp-seo-next/ai/credits');
            $out['saas_sync']['ai_credits_path'] = $aiCreditsPath !== ''
                ? $aiCreditsPath
                : (string) ($out['saas_sync']['ai_credits_path'] ?? '/wp-seo-next/ai/credits');

            $aiCreditsTimeout = isset($ss['ai_credits_timeout']) ? (int) $ss['ai_credits_timeout'] : (int) ($out['saas_sync']['ai_credits_timeout'] ?? 20);
            $out['saas_sync']['ai_credits_timeout'] = max(3, min(60, $aiCreditsTimeout));

            // Optional: Site-wide crawl analyzer endpoint.
            $crawlPath = isset($ss['crawl_path'])
                ? sanitize_text_field((string) $ss['crawl_path'])
                : (string) ($out['saas_sync']['crawl_path'] ?? '/wp-seo-next/crawl');
            $out['saas_sync']['crawl_path'] = $crawlPath !== ''
                ? $crawlPath
                : (string) ($out['saas_sync']['crawl_path'] ?? '/wp-seo-next/crawl');

            $crawlTimeout = isset($ss['crawl_timeout']) ? (int) $ss['crawl_timeout'] : (int) ($out['saas_sync']['crawl_timeout'] ?? 30);
            $out['saas_sync']['crawl_timeout'] = max(3, min(120, $crawlTimeout));

            // Optional: Cloud keyword clustering provider endpoint.
            $clusteringPath = isset($ss['clustering_path'])
                ? sanitize_text_field((string) $ss['clustering_path'])
                : (string) ($out['saas_sync']['clustering_path'] ?? '/wp-seo-next/clustering/build');
            $out['saas_sync']['clustering_path'] = $clusteringPath !== ''
                ? $clusteringPath
                : (string) ($out['saas_sync']['clustering_path'] ?? '/wp-seo-next/clustering/build');

            $clusteringTimeout = isset($ss['clustering_timeout']) ? (int) $ss['clustering_timeout'] : (int) ($out['saas_sync']['clustering_timeout'] ?? 30);
            $out['saas_sync']['clustering_timeout'] = max(3, min(120, $clusteringTimeout));
        }

        // SaaS Dashboard connector (optional).
        if (isset($input['saas_dashboard']) && is_array($input['saas_dashboard'])) {
            $sd = $input['saas_dashboard'];

            $out['saas_dashboard']['enabled'] = !empty($sd['enabled']);
            $out['saas_dashboard']['scheduled_sync'] = !empty($sd['scheduled_sync']);

            $allowedSchedules = ['hourly', 'twicedaily', 'daily', 'wpsn_15min', 'wpsn_10min', 'wpsn_5min'];
            $schedule = isset($sd['schedule']) ? sanitize_key((string) $sd['schedule']) : (string) $out['saas_dashboard']['schedule'];
            $out['saas_dashboard']['schedule'] = in_array($schedule, $allowedSchedules, true) ? $schedule : (string) $out['saas_dashboard']['schedule'];

            $timeout = isset($sd['timeout']) ? (int) $sd['timeout'] : (int) $out['saas_dashboard']['timeout'];
            $out['saas_dashboard']['timeout'] = max(3, min(60, $timeout));

            $out['saas_dashboard']['require_signed_tasks'] = !empty($sd['require_signed_tasks']);

            $healthPath = isset($sd['health_path']) ? sanitize_text_field((string) $sd['health_path']) : (string) $out['saas_dashboard']['health_path'];
            $tasksPath = isset($sd['tasks_path']) ? sanitize_text_field((string) $sd['tasks_path']) : (string) $out['saas_dashboard']['tasks_path'];
            $ackPath = isset($sd['ack_path']) ? sanitize_text_field((string) $sd['ack_path']) : (string) $out['saas_dashboard']['ack_path'];

            $out['saas_dashboard']['health_path'] = $healthPath !== '' ? $healthPath : (string) $out['saas_dashboard']['health_path'];
            $out['saas_dashboard']['tasks_path'] = $tasksPath !== '' ? $tasksPath : (string) $out['saas_dashboard']['tasks_path'];
            $out['saas_dashboard']['ack_path'] = $ackPath !== '' ? $ackPath : (string) $out['saas_dashboard']['ack_path'];
        }

        // SERP gap analyzer (optional)
        if (isset($input['serp_gap']) && is_array($input['serp_gap'])) {
            $sg = $input['serp_gap'];

            $pid = isset($sg['provider_id']) ? sanitize_key((string) $sg['provider_id']) : (string) $out['serp_gap']['provider_id'];
            $out['serp_gap']['provider_id'] = $pid !== '' ? $pid : (string) $out['serp_gap']['provider_id'];

            $lim = isset($sg['limit']) ? (int) $sg['limit'] : (int) $out['serp_gap']['limit'];
            $out['serp_gap']['limit'] = max(3, min(20, $lim));

            $depth = isset($sg['depth']) ? sanitize_key((string) $sg['depth']) : (string) ($out['serp_gap']['depth'] ?? 'quick');
            $out['serp_gap']['depth'] = in_array($depth, ['quick', 'deep'], true) ? $depth : 'quick';

            if (isset($sg['deep']) && is_array($sg['deep'])) {
                $deep = $sg['deep'];

                $to = isset($deep['timeout']) ? (int) $deep['timeout'] : (int) ($out['serp_gap']['deep']['timeout'] ?? 8);
                $out['serp_gap']['deep']['timeout'] = max(3, min(60, $to));

                $mb = isset($deep['max_html_bytes']) ? (int) $deep['max_html_bytes'] : (int) ($out['serp_gap']['deep']['max_html_bytes'] ?? 2000000);
                $out['serp_gap']['deep']['max_html_bytes'] = max(200000, min(8000000, $mb));
            }

            // Serper.dev provider settings (non-secret).
            if (isset($sg['serper']) && is_array($sg['serper'])) {
                $sp = $sg['serper'];

                $ep = isset($sp['endpoint']) ? esc_url_raw((string) $sp['endpoint']) : (string) ($out['serp_gap']['serper']['endpoint'] ?? '');
                $out['serp_gap']['serper']['endpoint'] = $ep !== '' ? $ep : 'https://google.serper.dev/search';

                $gl = isset($sp['gl']) ? sanitize_key((string) $sp['gl']) : (string) ($out['serp_gap']['serper']['gl'] ?? '');
                $hl = isset($sp['hl']) ? sanitize_key((string) $sp['hl']) : (string) ($out['serp_gap']['serper']['hl'] ?? '');
                $out['serp_gap']['serper']['gl'] = $gl;
                $out['serp_gap']['serper']['hl'] = $hl;

                $num = isset($sp['num']) ? (int) $sp['num'] : (int) ($out['serp_gap']['serper']['num'] ?? 10);
                $out['serp_gap']['serper']['num'] = max(1, min(20, $num));

                $rpm = isset($sp['rpm_limit']) ? (int) $sp['rpm_limit'] : (int) ($out['serp_gap']['serper']['rpm_limit'] ?? 60);
                $out['serp_gap']['serper']['rpm_limit'] = max(0, min(10000, $rpm));

                $bo = isset($sp['backoff_seconds']) ? (int) $sp['backoff_seconds'] : (int) ($out['serp_gap']['serper']['backoff_seconds'] ?? 30);
                $out['serp_gap']['serper']['backoff_seconds'] = max(5, min(3600, $bo));

                $to = isset($sp['timeout']) ? (int) $sp['timeout'] : (int) ($out['serp_gap']['serper']['timeout'] ?? 15);
                $out['serp_gap']['serper']['timeout'] = max(3, min(60, $to));
            }
        }

        // Breadcrumbs
        if (isset($input['breadcrumbs']) && is_array($input['breadcrumbs'])) {
            $bc = $input['breadcrumbs'];
            $out['breadcrumbs']['ui_enabled'] = !empty($bc['ui_enabled']);
            $out['breadcrumbs']['schema_enabled'] = !empty($bc['schema_enabled']);

            $hl = isset($bc['home_label']) ? sanitize_text_field((string) $bc['home_label']) : (string) ($out['breadcrumbs']['home_label'] ?? '');
            $hl = trim($hl);
            if (strlen($hl) > 80) {
                $hl = substr($hl, 0, 80);
            }
            $out['breadcrumbs']['home_label'] = $hl;

            $trailPrefix = isset($bc['trail_prefix']) ? sanitize_text_field((string) $bc['trail_prefix']) : (string) ($out['breadcrumbs']['trail_prefix'] ?? '');
            $trailPrefix = trim($trailPrefix);
            if (strlen($trailPrefix) > 120) {
                $trailPrefix = substr($trailPrefix, 0, 120);
            }
            $out['breadcrumbs']['trail_prefix'] = $trailPrefix;

            $archivePrefix = isset($bc['archive_prefix']) ? sanitize_text_field((string) $bc['archive_prefix']) : (string) ($out['breadcrumbs']['archive_prefix'] ?? '');
            $archivePrefix = trim($archivePrefix);
            if (strlen($archivePrefix) > 120) {
                $archivePrefix = substr($archivePrefix, 0, 120);
            }
            $out['breadcrumbs']['archive_prefix'] = $archivePrefix;

            $searchPrefix = isset($bc['search_prefix']) ? sanitize_text_field((string) $bc['search_prefix']) : (string) ($out['breadcrumbs']['search_prefix'] ?? '');
            $searchPrefix = trim($searchPrefix);
            if (strlen($searchPrefix) > 120) {
                $searchPrefix = substr($searchPrefix, 0, 120);
            }
            $out['breadcrumbs']['search_prefix'] = $searchPrefix;

            $notFoundLabel = isset($bc['not_found_label']) ? sanitize_text_field((string) $bc['not_found_label']) : (string) ($out['breadcrumbs']['not_found_label'] ?? '');
            $notFoundLabel = trim($notFoundLabel);
            if (strlen($notFoundLabel) > 120) {
                $notFoundLabel = substr($notFoundLabel, 0, 120);
            }
            $out['breadcrumbs']['not_found_label'] = $notFoundLabel;

            $out['breadcrumbs']['auto_insert'] = !empty($bc['auto_insert']);

            $pos = isset($bc['position']) ? sanitize_key((string) $bc['position']) : (string) $out['breadcrumbs']['position'];
            $out['breadcrumbs']['position'] = in_array($pos, ['before', 'after'], true) ? $pos : 'before';

            $sep = isset($bc['separator']) ? sanitize_text_field((string) $bc['separator']) : (string) $out['breadcrumbs']['separator'];
            $sep = trim($sep);
            $out['breadcrumbs']['separator'] = $sep !== '' ? $sep : (string) $out['breadcrumbs']['separator'];

            $out['breadcrumbs']['show_current'] = !empty($bc['show_current']);
            $out['breadcrumbs']['singular_only'] = !empty($bc['singular_only']);
        }

        // WooCommerce
        if (isset($input['woocommerce']) && is_array($input['woocommerce'])) {
            $wc = $input['woocommerce'];

            if (isset($wc['schema']) && is_array($wc['schema'])) {
                $out['woocommerce']['schema']['enabled'] = !empty($wc['schema']['enabled']);
                $out['woocommerce']['schema']['disable_wc_structured_data'] = !empty($wc['schema']['disable_wc_structured_data']);
            }

            if (isset($wc['social']) && is_array($wc['social'])) {
                $out['woocommerce']['social']['enabled'] = !empty($wc['social']['enabled']);
                $out['woocommerce']['social']['set_og_type_product'] = !empty($wc['social']['set_og_type_product']);
                $out['woocommerce']['social']['include_price'] = !empty($wc['social']['include_price']);
                $out['woocommerce']['social']['include_availability'] = !empty($wc['social']['include_availability']);
            }

            if (isset($wc['canonical']) && is_array($wc['canonical'])) {
                $can = $wc['canonical'];
                $out['woocommerce']['canonical']['strip_filter_args'] = !empty($can['strip_filter_args']);
                $out['woocommerce']['canonical']['noindex_filtered_archives'] = !empty($can['noindex_filtered_archives']);

                $extra = isset($can['extra_strip_args']) ? (string) $can['extra_strip_args'] : '';
                // Store as comma-separated string (sanitized), parse later.
                $extra = sanitize_text_field($extra);
                $extra = trim($extra);
                // Keep it short.
                if (strlen($extra) > 500) {
                    $extra = substr($extra, 0, 500);
                }
                $out['woocommerce']['canonical']['extra_strip_args'] = $extra;
            }

            if (isset($wc['merchant_feed']) && is_array($wc['merchant_feed'])) {
                $mf = $wc['merchant_feed'];

                $out['woocommerce']['merchant_feed']['enabled'] = !empty($mf['enabled']);
                $out['woocommerce']['merchant_feed']['include_variations'] = !empty($mf['include_variations']);
                $out['woocommerce']['merchant_feed']['in_stock_only'] = !empty($mf['in_stock_only']);
                $out['woocommerce']['merchant_feed']['exclude_hidden'] = !empty($mf['exclude_hidden']);

                $pp = isset($mf['posts_per_page']) ? (int) $mf['posts_per_page'] : (int) ($out['woocommerce']['merchant_feed']['posts_per_page'] ?? 200);
                if ($pp < 10) {
                    $pp = 10;
                }
                if ($pp > 2000) {
                    $pp = 2000;
                }
                $out['woocommerce']['merchant_feed']['posts_per_page'] = $pp;

                $max = isset($mf['max_products']) ? (int) $mf['max_products'] : 0;
                if ($max < 0) {
                    $max = 0;
                }
                if ($max > 500000) {
                    $max = 500000;
                }
                $out['woocommerce']['merchant_feed']['max_products'] = $max;

                $cond = isset($mf['condition']) ? strtolower(trim((string) $mf['condition'])) : 'new';
                if (!in_array($cond, ['new', 'used', 'refurbished'], true)) {
                    $cond = 'new';
                }
                $out['woocommerce']['merchant_feed']['condition'] = $cond;

                $ttl = isset($mf['cache_ttl_sec']) ? (int) $mf['cache_ttl_sec'] : 0;
                if ($ttl < 0) {
                    $ttl = 0;
                }
                if ($ttl > 6 * HOUR_IN_SECONDS) {
                    $ttl = 6 * HOUR_IN_SECONDS;
                }
                $out['woocommerce']['merchant_feed']['cache_ttl_sec'] = $ttl;
            }
        }

        // Local SEO (business info + LocalBusiness schema + locations KML/sitemap)
        if (isset($input['local']) && is_array($input['local'])) {
            $loc = $input['local'];

            if (isset($loc['schema']) && is_array($loc['schema'])) {
                $out['local']['schema']['enabled'] = !empty($loc['schema']['enabled']);
                $out['local']['schema']['show_on_home'] = !empty($loc['schema']['show_on_home']);

                $type = isset($loc['schema']['business_type']) ? sanitize_text_field((string) $loc['schema']['business_type']) : (string) $out['local']['schema']['business_type'];
                $type = trim($type);
                // Keep it simple: allow letters only and fallback.
                if ($type === '') {
                    $type = 'LocalBusiness';
                }
                $out['local']['schema']['business_type'] = $type;
            }

            // Primary location (Location post ID). Used for home-page schema + NAP defaults.
            if (isset($loc['primary_location_id'])) {
                $pid = (int) $loc['primary_location_id'];
                if ($pid < 0) {
                    $pid = 0;
                }
                if ($pid > 0) {
                    // Validate only if the post exists and matches our CPT.
                    $pt = get_post_type($pid);
                    $st = get_post_status($pid);
                    if ($pt !== 'wpsn_location' || $st !== 'publish') {
                        $pid = 0;
                    }
                }
                $out['local']['primary_location_id'] = $pid;
            }

            // NAP output defaults (shortcode / NAP block)
            if (isset($loc['nap']) && is_array($loc['nap'])) {
                $nap = $loc['nap'];

                $tpl = isset($nap['template']) ? strtolower(trim((string) $nap['template'])) : (string) ($out['local']['nap']['template'] ?? 'plain');
                if (!in_array($tpl, ['plain', 'multiline', 'icons', 'icons_multiline'], true)) {
                    $tpl = 'plain';
                }
                $out['local']['nap']['template'] = $tpl;

                $out['local']['nap']['microdata'] = !empty($nap['microdata']);
            }

            if (isset($loc['kml']) && is_array($loc['kml'])) {
                $out['local']['kml']['enabled'] = !empty($loc['kml']['enabled']);
            }

            if (isset($loc['locations_sitemap']) && is_array($loc['locations_sitemap'])) {
                $out['local']['locations_sitemap']['enabled'] = !empty($loc['locations_sitemap']['enabled']);
            }

            if (isset($loc['business']) && is_array($loc['business'])) {
                $b = $loc['business'];

                $out['local']['business']['name'] = isset($b['name']) ? sanitize_text_field((string) $b['name']) : (string) $out['local']['business']['name'];
                $out['local']['business']['telephone'] = isset($b['telephone']) ? sanitize_text_field((string) $b['telephone']) : (string) $out['local']['business']['telephone'];
                $out['local']['business']['email'] = isset($b['email']) ? sanitize_email((string) $b['email']) : (string) $out['local']['business']['email'];
                $out['local']['business']['street_address'] = isset($b['street_address']) ? sanitize_text_field((string) $b['street_address']) : (string) $out['local']['business']['street_address'];
                $out['local']['business']['address_locality'] = isset($b['address_locality']) ? sanitize_text_field((string) $b['address_locality']) : (string) $out['local']['business']['address_locality'];
                $out['local']['business']['address_region'] = isset($b['address_region']) ? sanitize_text_field((string) $b['address_region']) : (string) $out['local']['business']['address_region'];
                $out['local']['business']['postal_code'] = isset($b['postal_code']) ? sanitize_text_field((string) $b['postal_code']) : (string) $out['local']['business']['postal_code'];
                $out['local']['business']['address_country'] = isset($b['address_country']) ? sanitize_text_field((string) $b['address_country']) : (string) $out['local']['business']['address_country'];
                $out['local']['business']['price_range'] = isset($b['price_range']) ? sanitize_text_field((string) $b['price_range']) : (string) $out['local']['business']['price_range'];

                // Coordinates (stored as strings to preserve decimals).
                $lat = isset($b['latitude']) ? trim((string) $b['latitude']) : '';
                $lng = isset($b['longitude']) ? trim((string) $b['longitude']) : '';
                $lat = preg_match('/^-?\d{1,3}(\.\d+)?$/', $lat) ? $lat : '';
                $lng = preg_match('/^-?\d{1,3}(\.\d+)?$/', $lng) ? $lng : '';
                $out['local']['business']['latitude'] = $lat;
                $out['local']['business']['longitude'] = $lng;

                $oh = isset($b['opening_hours']) ? sanitize_textarea_field((string) $b['opening_hours']) : (string) $out['local']['business']['opening_hours'];
                $out['local']['business']['opening_hours'] = trim($oh);
            }
        }

        // Media SEO
        if (isset($input['media']) && is_array($input['media'])) {
            $media = $input['media'];

            if (isset($media['filenames']) && is_array($media['filenames'])) {
                $fn = $media['filenames'];

                // Dirty detector groups.
                if (isset($fn['dirty_groups']) && is_array($fn['dirty_groups'])) {
                    foreach (['camera', 'screenshot', 'messenger', 'editor', 'untitled', 'numeric', 'hash'] as $k) {
                        $out['media']['filenames']['dirty_groups'][$k] = !empty($fn['dirty_groups'][$k]);
                    }
                }

                // Custom prefixes (one per line).
                $custom = isset($fn['custom_prefixes']) ? sanitize_textarea_field((string) $fn['custom_prefixes']) : '';
                $custom = trim($custom);
                if ($custom !== '') {
                    $lines = preg_split('/\r\n|\r|\n/', $custom) ?: [];
                    $clean = [];
                    foreach ($lines as $line) {
                        $line = strtolower(trim((string) $line));
                        if ($line === '') {
                            continue;
                        }
                        // Keep it simple: letters/numbers/hyphen/underscore only.
                        $line = (string) preg_replace('/[^a-z0-9_\-]/', '', $line);
                        $line = trim($line, '-_');
                        if ($line !== '') {
                            $clean[] = $line;
                        }
                    }
                    $clean = array_values(array_unique($clean));
                    $clean = array_slice($clean, 0, 50);
                    $custom = implode("\n", $clean);
                }
                $out['media']['filenames']['custom_prefixes'] = $custom;
            }
        }

        // Sitemap
        if (isset($input['sitemap']) && is_array($input['sitemap'])) {
            $sm = $input['sitemap'];

            $per = isset($sm['urls_per_sitemap']) ? (int) $sm['urls_per_sitemap'] : (int) $out['sitemap']['urls_per_sitemap'];
            if ($per < 100) {
                $per = 100;
            }
            if ($per > 50000) {
                $per = 50000;
            }
            $out['sitemap']['urls_per_sitemap'] = $per;

            $out['sitemap']['include_taxonomies'] = !empty($sm['include_taxonomies']);

            // post_types: map of slug => bool
            $out['sitemap']['post_types'] = [];
            if (isset($sm['post_types']) && is_array($sm['post_types'])) {
                foreach ($sm['post_types'] as $pt => $enabled) {
                    $pt = sanitize_key((string) $pt);
                    if ($pt === '') {
                        continue;
                    }
                    $out['sitemap']['post_types'][$pt] = (bool) $enabled;
                }
            }

            // taxonomies: map of slug => bool
            $out['sitemap']['taxonomies'] = [];
            if (isset($sm['taxonomies']) && is_array($sm['taxonomies'])) {
                foreach ($sm['taxonomies'] as $tx => $enabled) {
                    $tx = sanitize_key((string) $tx);
                    if ($tx === '') {
                        continue;
                    }
                    $out['sitemap']['taxonomies'][$tx] = (bool) $enabled;
                }
            }

            // Images: add <image:image> tags in post sitemaps.
            if (isset($sm['images']) && is_array($sm['images'])) {
                $im = $sm['images'];
                $out['sitemap']['images']['enabled'] = !empty($im['enabled']);
                $out['sitemap']['images']['include_featured'] = !empty($im['include_featured']);
                $out['sitemap']['images']['include_content'] = !empty($im['include_content']);

                $max = isset($im['max_per_url']) ? (int) $im['max_per_url'] : (int) $out['sitemap']['images']['max_per_url'];
                if ($max < 1) {
                    $max = 1;
                }
                if ($max > 100) {
                    $max = 100;
                }
                $out['sitemap']['images']['max_per_url'] = $max;
            }

            // Google News sitemap.
            if (isset($sm['news']) && is_array($sm['news'])) {
                $news = $sm['news'];
                $out['sitemap']['news']['enabled'] = !empty($news['enabled']);

                $out['sitemap']['news']['publication_name'] = isset($news['publication_name'])
                    ? sanitize_text_field((string) $news['publication_name'])
                    : (string) $out['sitemap']['news']['publication_name'];

                $lang = isset($news['language']) ? (string) $news['language'] : '';
                $lang = strtolower(trim($lang));
                // allow en / en-us / pt-br, keep letters + dash only.
                $lang = preg_replace('/[^a-z\-]/', '', $lang);
                if (!is_string($lang)) {
                    $lang = '';
                }
                if (strlen($lang) > 10) {
                    $lang = substr($lang, 0, 10);
                }
                $out['sitemap']['news']['language'] = $lang;

                $out['sitemap']['news']['genres'] = isset($news['genres'])
                    ? sanitize_text_field((string) $news['genres'])
                    : (string) $out['sitemap']['news']['genres'];

                $out['sitemap']['news']['include_keywords'] = !empty($news['include_keywords']);

                // post_types: map of slug => bool
                $out['sitemap']['news']['post_types'] = [];
                if (isset($news['post_types']) && is_array($news['post_types'])) {
                    foreach ($news['post_types'] as $pt => $enabled) {
                        $pt = sanitize_key((string) $pt);
                        if ($pt === '') {
                            continue;
                        }
                        $out['sitemap']['news']['post_types'][$pt] = (bool) $enabled;
                    }
                }
            }

            // Video sitemap.
            if (isset($sm['video']) && is_array($sm['video'])) {
                $video = $sm['video'];
                $out['sitemap']['video']['enabled'] = !empty($video['enabled']);

                $mode = isset($video['mode']) ? sanitize_key((string) $video['mode']) : 'meta';
                $out['sitemap']['video']['mode'] = in_array($mode, ['meta', 'youtube_auto'], true) ? $mode : 'meta';

                $out['sitemap']['video']['include_featured_thumbnail'] = !empty($video['include_featured_thumbnail']);

                // post_types: map of slug => bool
                $out['sitemap']['video']['post_types'] = [];
                if (isset($video['post_types']) && is_array($video['post_types'])) {
                    foreach ($video['post_types'] as $pt => $enabled) {
                        $pt = sanitize_key((string) $pt);
                        if ($pt === '') {
                            continue;
                        }
                        $out['sitemap']['video']['post_types'][$pt] = (bool) $enabled;
                    }
                }
            }
        }

        // robots.txt helpers
        if (isset($input['robots_txt']) && is_array($input['robots_txt'])) {
            $rt = $input['robots_txt'];
            $out['robots_txt']['append_sitemap'] = !empty($rt['append_sitemap']);

            $custom = isset($rt['custom']) ? sanitize_textarea_field((string) $rt['custom']) : '';
            $out['robots_txt']['custom'] = $this->sanitizeRobotsTxtCustom($custom);

            // Structured User-agent blocks.
            if (isset($rt['agent_groups']) && is_array($rt['agent_groups'])) {
                $groupsOut = [];
                $seen = [];

                foreach ($rt['agent_groups'] as $group) {
                    if (!is_array($group)) {
                        continue;
                    }

                    $id = isset($group['id']) ? sanitize_text_field((string) $group['id']) : '';
                    $id = $id !== '' ? $id : wp_generate_uuid4();

                    $enabled = !empty($group['enabled']);
                    $userAgent = isset($group['user_agent']) ? \WPSN\Modules\Robots\RobotsUserAgentRules::normalizeUserAgent((string) $group['user_agent']) : '';
                    if ($userAgent === '') {
                        continue;
                    }

                    $linesIn = $group['lines'] ?? [];
                    if (is_string($linesIn)) {
                        $linesIn = preg_split('/\r\n|\r|\n/', $linesIn) ?: [];
                    }
                    if (!is_array($linesIn)) {
                        continue;
                    }

                    $lines = \WPSN\Modules\Robots\RobotsUserAgentRules::normalizeLines($linesIn);
                    if (empty($lines)) {
                        continue;
                    }

                    $fingerprint = strtolower($userAgent . "\n" . implode("\n", $lines));
                    if (isset($seen[$fingerprint])) {
                        continue;
                    }
                    $seen[$fingerprint] = true;

                    $source = isset($group['source']) ? sanitize_key((string) $group['source']) : '';
                    $note = isset($group['note']) ? sanitize_text_field((string) $group['note']) : '';
                    $createdAt = isset($group['created_at']) ? sanitize_text_field((string) $group['created_at']) : '';

                    $groupsOut[] = [
                        'id' => $id,
                        'enabled' => $enabled,
                        'user_agent' => $userAgent,
                        'lines' => $lines,
                        'source' => $source,
                        'note' => $note,
                        'created_at' => $createdAt,
                    ];

                    if (count($groupsOut) >= 100) {
                        break;
                    }
                }

                $out['robots_txt']['agent_groups'] = $groupsOut;
            }

            // Managed Disallow/Allow rules (structured list).
            if (isset($rt['disallow_rules']) && is_array($rt['disallow_rules'])) {
                $rulesIn = $rt['disallow_rules'];
                $rulesOut = [];
                $seen = [];

                foreach ($rulesIn as $r) {
                    if (!is_array($r)) {
                        continue;
                    }

                    $id = isset($r['id']) ? sanitize_text_field((string) $r['id']) : '';
                    $id = $id !== '' ? $id : wp_generate_uuid4();

                    $enabled = !empty($r['enabled']);

                    $line = isset($r['line']) ? trim((string) $r['line']) : '';
                    $line = preg_replace('/\s+/', ' ', $line);
                    $line = is_string($line) ? trim($line) : '';

                    if ($line === '') {
                        continue;
                    }

                    // Only allow Allow/Disallow directives.
                    $low = strtolower($line);
                    if (strpos($low, 'disallow:') === 0) {
                        $val = trim(substr($line, strlen('disallow:')));
                        if ($val === '') {
                            continue;
                        }
                        $line = 'Disallow: ' . $val;
                    } elseif (strpos($low, 'allow:') === 0) {
                        $val = trim(substr($line, strlen('allow:')));
                        if ($val === '') {
                            continue;
                        }
                        $line = 'Allow: ' . $val;
                    } else {
                        continue;
                    }

                    $key = strtolower($line);
                    if (isset($seen[$key])) {
                        continue;
                    }
                    $seen[$key] = true;

                    $source = isset($r['source']) ? sanitize_key((string) $r['source']) : '';
                    $note = isset($r['note']) ? sanitize_text_field((string) $r['note']) : '';
                    $createdAt = isset($r['created_at']) ? sanitize_text_field((string) $r['created_at']) : '';

                    $rulesOut[] = [
                        'id' => $id,
                        'enabled' => $enabled,
                        'line' => $line,
                        'source' => $source,
                        'note' => $note,
                        'created_at' => $createdAt,
                    ];

                    // Hard cap.
                    if (count($rulesOut) >= 500) {
                        break;
                    }
                }

                $out['robots_txt']['disallow_rules'] = $rulesOut;
            }
        }

        // Search URL canonicalization (opt-in).
        if (isset($input['search_urls']) && is_array($input['search_urls'])) {
            $searchUrls = $input['search_urls'];

            $out['search_urls']['enabled'] = !empty($searchUrls['enabled']);
            $out['search_urls']['strip_category_base'] = !empty($searchUrls['strip_category_base']);
            $out['search_urls']['sanitize_query'] = !empty($searchUrls['sanitize_query']);
            $out['search_urls']['strip_emoji'] = !empty($searchUrls['strip_emoji']);
            $out['search_urls']['block_common_patterns'] = !empty($searchUrls['block_common_patterns']);

            $out['search_urls']['query_limit'] = $this->clampInt($searchUrls['query_limit'] ?? $out['search_urls']['query_limit'], 0, 200);

            $invalidAction = isset($searchUrls['invalid_action']) ? sanitize_key((string) $searchUrls['invalid_action']) : '404';
            $allowedInvalidActions = ['redirect_home', '404', 'empty_search'];
            $out['search_urls']['invalid_action'] = in_array($invalidAction, $allowedInvalidActions, true) ? $invalidAction : '404';

            $mode = isset($searchUrls['mode']) ? sanitize_key((string) $searchUrls['mode']) : 'format';
            $allowedModes = ['format', 'strict'];
            $out['search_urls']['mode'] = in_array($mode, $allowedModes, true) ? $mode : 'format';

            $preserve = isset($searchUrls['preserve_args']) ? (string) $searchUrls['preserve_args'] : '';
            $items = preg_split('/[\s,]+/', strtolower($preserve)) ?: [];
            $clean = [];

            foreach ($items as $item) {
                $key = sanitize_key((string) $item);
                if ($key !== '') {
                    $clean[$key] = $key;
                }
            }

            $out['search_urls']['preserve_args'] = implode("\n", array_values($clean));

            $blacklistRaw = isset($searchUrls['blacklist_patterns']) ? (string) $searchUrls['blacklist_patterns'] : '';
            $out['search_urls']['blacklist_patterns'] = $this->sanitizeSearchUrlBlacklistPatterns($blacklistRaw);
        }

        // Hreflang alternates (opt-in).
        if (isset($input['hreflang']) && is_array($input['hreflang'])) {
            $hreflang = $input['hreflang'];

            $out['hreflang']['enabled'] = !empty($hreflang['enabled']);
            $out['hreflang']['include_x_default'] = !empty($hreflang['include_x_default']);

            $xDefaultUrl = isset($hreflang['x_default_url']) ? trim((string) $hreflang['x_default_url']) : '';
            if ($xDefaultUrl !== '' && strpos($xDefaultUrl, '/') === 0 && strpos($xDefaultUrl, '//') !== 0) {
                $xDefaultUrl = home_url($xDefaultUrl);
            }
            $out['hreflang']['x_default_url'] = $xDefaultUrl !== '' ? esc_url_raw($xDefaultUrl) : '';
        }

        // Head cleanup / hardening (opt-in).
        if (isset($input['head_cleanup']) && is_array($input['head_cleanup'])) {
            $out['head_cleanup'] = HeadCleanupProfile::sanitize($input['head_cleanup']);
        }

        // AI settings (10A/10B)
        if (isset($input['ai']) && is_array($input['ai'])) {
            $ai = $input['ai'];

            if (isset($ai['local']) && is_array($ai['local'])) {
                $local = $ai['local'];
                $out['ai']['local']['enabled'] = !empty($local['enabled']);

                $out['ai']['local']['title_max_length'] = $this->clampInt($local['title_max_length'] ?? $out['ai']['local']['title_max_length'], 20, 120);
                $out['ai']['local']['description_max_length'] = $this->clampInt($local['description_max_length'] ?? $out['ai']['local']['description_max_length'], 50, 400);
                $out['ai']['local']['keywords_limit'] = $this->clampInt($local['keywords_limit'] ?? $out['ai']['local']['keywords_limit'], 5, 50);
                $out['ai']['local']['max_content_chars'] = $this->clampInt($local['max_content_chars'] ?? $out['ai']['local']['max_content_chars'], 2000, 50000);
            }

            if (isset($ai['privacy']) && is_array($ai['privacy'])) {
                $priv = $ai['privacy'];
                $out['ai']['privacy']['no_drafts'] = !empty($priv['no_drafts']);
                $out['ai']['privacy']['redact_pii'] = !empty($priv['redact_pii']);
            }

            if (isset($ai['credits']) && is_array($ai['credits'])) {
                $cr = $ai['credits'];

                $out['ai']['credits']['enabled'] = !empty($cr['enabled']);

                $mode = isset($cr['mode']) ? sanitize_key((string) $cr['mode']) : (string) $out['ai']['credits']['mode'];
                if (!in_array($mode, ['tokens', 'usd'], true)) {
                    $mode = (string) $out['ai']['credits']['mode'];
                }
                $out['ai']['credits']['mode'] = $mode;

                $tpc = isset($cr['tokens_per_credit']) ? (int) $cr['tokens_per_credit'] : (int) $out['ai']['credits']['tokens_per_credit'];
                if ($tpc < 100) {
                    $tpc = 100;
                }
                if ($tpc > 1000000) {
                    $tpc = 1000000;
                }
                $out['ai']['credits']['tokens_per_credit'] = $tpc;

                $cpu = $this->parseFloat($cr['credits_per_usd'] ?? $out['ai']['credits']['credits_per_usd']);
                if ($cpu < 0.0) {
                    $cpu = 0.0;
                }
                if ($cpu > 10000000.0) {
                    $cpu = 10000000.0;
                }
                $out['ai']['credits']['credits_per_usd'] = $cpu;
            }

            if (isset($ai['external']) && is_array($ai['external'])) {
                $ext = $ai['external'];

                $out['ai']['external']['enabled'] = !empty($ext['enabled']);

                $provider = isset($ext['provider']) ? sanitize_key((string) $ext['provider']) : 'openai';
                $out['ai']['external']['provider'] = in_array($provider, ['openai', 'saas_proxy'], true) ? $provider : 'openai';

                $out['ai']['external']['model'] = isset($ext['model']) ? sanitize_text_field((string) $ext['model']) : $out['ai']['external']['model'];
                $out['ai']['external']['max_output_tokens'] = $this->clampInt($ext['max_output_tokens'] ?? $out['ai']['external']['max_output_tokens'], 16, 2048);
                $out['ai']['external']['brief_max_output_tokens'] = $this->clampInt($ext['brief_max_output_tokens'] ?? $out['ai']['external']['brief_max_output_tokens'], 64, 4096);

                $temp = $this->parseFloat($ext['temperature'] ?? $out['ai']['external']['temperature']);
                if ($temp < 0.0) {
                    $temp = 0.0;
                }
                if ($temp > 2.0) {
                    $temp = 2.0;
                }
                $out['ai']['external']['temperature'] = $temp;

                $out['ai']['external']['daily_token_limit'] = $this->clampInt($ext['daily_token_limit'] ?? $out['ai']['external']['daily_token_limit'], 0, 100000000);
                $out['ai']['external']['daily_request_limit'] = $this->clampInt($ext['daily_request_limit'] ?? $out['ai']['external']['daily_request_limit'], 0, 1000000);

                $budget = $this->parseFloat($ext['daily_budget_usd'] ?? $out['ai']['external']['daily_budget_usd']);
                if ($budget < 0.0) {
                    $budget = 0.0;
                }
                $out['ai']['external']['daily_budget_usd'] = $budget;

                $pi = $this->parseFloat($ext['price_input_per_1m'] ?? $out['ai']['external']['price_input_per_1m']);
                $po = $this->parseFloat($ext['price_output_per_1m'] ?? $out['ai']['external']['price_output_per_1m']);
                if ($pi < 0.0) {
                    $pi = 0.0;
                }
                if ($po < 0.0) {
                    $po = 0.0;
                }
                $out['ai']['external']['price_input_per_1m'] = $pi;
                $out['ai']['external']['price_output_per_1m'] = $po;

                $out['ai']['external']['store_responses'] = !empty($ext['store_responses']);

                $out['ai']['external']['openai_org'] = isset($ext['openai_org']) ? sanitize_text_field((string) $ext['openai_org']) : '';
                $out['ai']['external']['openai_project'] = isset($ext['openai_project']) ? sanitize_text_field((string) $ext['openai_project']) : '';

                // External AI REST rate limiting (best-effort).
                if (isset($ext['ratelimit']) && is_array($ext['ratelimit'])) {
                    $rl = $ext['ratelimit'];
                    $out['ai']['external']['ratelimit']['enabled'] = !empty($rl['enabled']);
                    $out['ai']['external']['ratelimit']['per_user'] = !empty($rl['per_user']);

                    $rpm = isset($rl['requests_per_minute']) ? (int) $rl['requests_per_minute'] : (int) ($out['ai']['external']['ratelimit']['requests_per_minute'] ?? 30);
                    if ($rpm < 0) {
                        $rpm = 0;
                    }
                    if ($rpm > 6000) {
                        $rpm = 6000;
                    }
                    $out['ai']['external']['ratelimit']['requests_per_minute'] = $rpm;
                }

            }

            if (isset($ai['prompts']) && is_array($ai['prompts'])) {
                $p = $ai['prompts'];

                $out['ai']['prompts']['title_template'] = isset($p['title_template'])
                    ? sanitize_textarea_field((string) $p['title_template'])
                    : $out['ai']['prompts']['title_template'];

                $out['ai']['prompts']['description_template'] = isset($p['description_template'])
                    ? sanitize_textarea_field((string) $p['description_template'])
                    : $out['ai']['prompts']['description_template'];

                $out['ai']['prompts']['alt_template'] = isset($p['alt_template'])
                    ? sanitize_textarea_field((string) $p['alt_template'])
                    : $out['ai']['prompts']['alt_template'];

                $out['ai']['prompts']['brief_template'] = isset($p['brief_template'])
                    ? sanitize_textarea_field((string) $p['brief_template'])
                    : $out['ai']['prompts']['brief_template'];

                // SERP Gap prompt templates.
                $out['ai']['prompts']['serp_gap_template'] = isset($p['serp_gap_template'])
                    ? sanitize_textarea_field((string) $p['serp_gap_template'])
                    : ($out['ai']['prompts']['serp_gap_template'] ?? '');

                $out['ai']['prompts']['serp_gap_faq_template'] = isset($p['serp_gap_faq_template'])
                    ? sanitize_textarea_field((string) $p['serp_gap_faq_template'])
                    : ($out['ai']['prompts']['serp_gap_faq_template'] ?? '');

                // Versions are handled in update() by comparing old vs new templates.
            }
        }

        // AI Search Readiness (llms.txt + AI crawler controls)
        if (isset($input['ai_readiness']) && is_array($input['ai_readiness'])) {
            $ar = $input['ai_readiness'];

            // llms.txt
            if (isset($ar['llms']) && is_array($ar['llms'])) {
                $llms = $ar['llms'];

                $out['ai_readiness']['llms']['enabled'] = !empty($llms['enabled']);

                $mode = isset($llms['mode']) ? sanitize_key((string) $llms['mode']) : 'auto';
                if (!in_array($mode, ['auto', 'direct', 'manual'], true)) {
                    $mode = 'auto';
                }
                $out['ai_readiness']['llms']['mode'] = $mode;

                $manual = isset($llms['manual']) ? sanitize_textarea_field((string) $llms['manual']) : '';
                // Keep a reasonable bound to avoid huge options payloads.
                if (function_exists('mb_strlen') && mb_strlen($manual) > 200000) {
                    $manual = (string) mb_substr($manual, 0, 200000);
                } elseif (strlen($manual) > 200000) {
                    $manual = substr($manual, 0, 200000);
                }
                $out['ai_readiness']['llms']['manual'] = $manual;

                if (isset($llms['auto']) && is_array($llms['auto'])) {
                    $auto = $llms['auto'];
                    $out['ai_readiness']['llms']['auto']['include_pages'] = !empty($auto['include_pages']);
                    $out['ai_readiness']['llms']['auto']['include_posts'] = !empty($auto['include_posts']);
                    $out['ai_readiness']['llms']['auto']['max_links'] = $this->clampInt($auto['max_links'] ?? $out['ai_readiness']['llms']['auto']['max_links'], 5, 200);
                }
            }

            // bots
            if (isset($ar['bots']) && is_array($ar['bots'])) {
                $bots = $ar['bots'];
                $out['ai_readiness']['bots']['enabled'] = !empty($bots['enabled']);

                $allow = isset($bots['allow']) && is_array($bots['allow']) ? $bots['allow'] : [];
                $allowedKeys = ['oai_searchbot', 'gptbot', 'chatgpt_user', 'claude_searchbot', 'claudebot', 'claude_user', 'google_extended'];
                foreach ($allowedKeys as $k) {
                    if (array_key_exists($k, $allow)) {
                        $out['ai_readiness']['bots']['allow'][$k] = !empty($allow[$k]);
                    }
                }

                // Bot Blocker (server-side): optional hard block for disallowed bots.
                if (isset($bots['blocker']) && is_array($bots['blocker'])) {
                    $b = $bots['blocker'];

                    $out['ai_readiness']['bots']['blocker']['enabled'] = !empty($b['enabled']);

                    $code = isset($b['status_code']) ? (int) $b['status_code'] : (int) $out['ai_readiness']['bots']['blocker']['status_code'];
                    $out['ai_readiness']['bots']['blocker']['status_code'] = in_array($code, [403, 429], true) ? $code : 403;

                    $out['ai_readiness']['bots']['blocker']['retry_after'] = $this->clampInt(
                        $b['retry_after'] ?? $out['ai_readiness']['bots']['blocker']['retry_after'],
                        1,
                        3600
                    );

                    $out['ai_readiness']['bots']['blocker']['allow_logged_in'] = !empty($b['allow_logged_in']);
                }
            }
        }

        // Automation SDK (REST + webhooks).
        if (isset($input['automation']) && is_array($input['automation'])) {
            $a = $input['automation'];

            if (isset($a['rest']) && is_array($a['rest'])) {
                $r = $a['rest'];

                $out['automation']['rest']['enabled'] = !empty($r['enabled']);

                if (isset($r['public_head']) && is_array($r['public_head'])) {
                    $ph = $r['public_head'];
                    $out['automation']['rest']['public_head']['enabled'] = !empty($ph['enabled']);
                    $out['automation']['rest']['public_head']['cache_ttl'] = $this->clampInt(
                        $ph['cache_ttl'] ?? $out['automation']['rest']['public_head']['cache_ttl'],
                        0,
                        86400
                    );
                }
            }

            if (isset($a['webhooks']) && is_array($a['webhooks'])) {
                $wh = $a['webhooks'];

                $out['automation']['webhooks']['meta_updated'] = isset($wh['meta_updated'])
                    ? $this->sanitizeWebhookUrl((string) $wh['meta_updated'])
                    : $out['automation']['webhooks']['meta_updated'];

                $out['automation']['webhooks']['brief_generated'] = isset($wh['brief_generated'])
                    ? $this->sanitizeWebhookUrl((string) $wh['brief_generated'])
                    : $out['automation']['webhooks']['brief_generated'];

                $out['automation']['webhooks']['redirect_created'] = isset($wh['redirect_created'])
                    ? $this->sanitizeWebhookUrl((string) $wh['redirect_created'])
                    : $out['automation']['webhooks']['redirect_created'];

                $out['automation']['webhooks']['redirect_updated'] = isset($wh['redirect_updated'])
                    ? $this->sanitizeWebhookUrl((string) $wh['redirect_updated'])
                    : $out['automation']['webhooks']['redirect_updated'];

                $out['automation']['webhooks']['audit_scan_completed'] = isset($wh['audit_scan_completed'])
                    ? $this->sanitizeWebhookUrl((string) $wh['audit_scan_completed'])
                    : $out['automation']['webhooks']['audit_scan_completed'];

                $out['automation']['webhooks']['schema_scan_completed'] = isset($wh['schema_scan_completed'])
                    ? $this->sanitizeWebhookUrl((string) $wh['schema_scan_completed'])
                    : $out['automation']['webhooks']['schema_scan_completed'];


                $out['automation']['webhooks']['schema_fix_completed'] = isset($wh['schema_fix_completed'])
                    ? $this->sanitizeWebhookUrl((string) $wh['schema_fix_completed'])
                    : $out['automation']['webhooks']['schema_fix_completed'];

                $out['automation']['webhooks']['schema_validation_failed'] = isset($wh['schema_validation_failed'])
                    ? $this->sanitizeWebhookUrl((string) $wh['schema_validation_failed'])
                    : $out['automation']['webhooks']['schema_validation_failed'];

                $out['automation']['webhooks']['secret'] = isset($wh['secret'])
                    ? sanitize_text_field((string) $wh['secret'])
                    : $out['automation']['webhooks']['secret'];

                $out['automation']['webhooks']['timeout'] = $this->clampInt(
                    $wh['timeout'] ?? $out['automation']['webhooks']['timeout'],
                    1,
                    30
                );
            }
        }

        // Search Console Hub (GSC Data Hub + reports + internal links).
        if (isset($input['search_console']) && is_array($input['search_console'])) {
            $sc = $input['search_console'];

            $out['search_console']['property'] = isset($sc['property']) ? sanitize_text_field((string) $sc['property']) : $out['search_console']['property'];
            $out['search_console']['range_days'] = $this->clampInt($sc['range_days'] ?? $out['search_console']['range_days'], 1, 180);
            $out['search_console']['row_limit'] = $this->clampInt($sc['row_limit'] ?? $out['search_console']['row_limit'], 1000, 25000);
            $out['search_console']['scheduled_sync'] = !empty($sc['scheduled_sync']);

            if (isset($sc['content_gap']) && is_array($sc['content_gap'])) {
                $cg = $sc['content_gap'];
                $out['search_console']['content_gap']['min_impressions'] = $this->clampInt($cg['min_impressions'] ?? $out['search_console']['content_gap']['min_impressions'], 0, 10000000);
                $out['search_console']['content_gap']['pos_min'] = $this->clampInt($cg['pos_min'] ?? $out['search_console']['content_gap']['pos_min'], 1, 100);
                $out['search_console']['content_gap']['pos_max'] = $this->clampInt($cg['pos_max'] ?? $out['search_console']['content_gap']['pos_max'], 1, 100);
                $ctr = $this->parseFloat($cg['ctr_max'] ?? $out['search_console']['content_gap']['ctr_max']);
                if ($ctr < 0.0) {
                    $ctr = 0.0;
                }
                if ($ctr > 1.0) {
                    $ctr = 1.0;
                }
                $out['search_console']['content_gap']['ctr_max'] = $ctr;
            }

            if (isset($sc['internal_links']) && is_array($sc['internal_links'])) {
                $il = $sc['internal_links'];
                $out['search_console']['internal_links']['enabled'] = !empty($il['enabled']);
                $out['search_console']['internal_links']['only_orphans'] = !empty($il['only_orphans']);
                $out['search_console']['internal_links']['min_impressions'] = $this->clampInt($il['min_impressions'] ?? $out['search_console']['internal_links']['min_impressions'], 0, 10000000);
                $out['search_console']['internal_links']['max_targets'] = $this->clampInt($il['max_targets'] ?? $out['search_console']['internal_links']['max_targets'], 1, 200);
                $out['search_console']['internal_links']['max_queries_per_target'] = $this->clampInt($il['max_queries_per_target'] ?? $out['search_console']['internal_links']['max_queries_per_target'], 1, 10);
                $out['search_console']['internal_links']['max_sources_per_query'] = $this->clampInt($il['max_sources_per_query'] ?? $out['search_console']['internal_links']['max_sources_per_query'], 1, 50);
                $out['search_console']['internal_links']['max_suggestions_per_target'] = $this->clampInt($il['max_suggestions_per_target'] ?? $out['search_console']['internal_links']['max_suggestions_per_target'], 1, 20);
            }

            // Rank tracking (daily snapshots + alerts).
            if (isset($sc['rank_tracking']) && is_array($sc['rank_tracking'])) {
                $rt = $sc['rank_tracking'];
                $out['search_console']['rank_tracking']['enabled'] = !empty($rt['enabled']);
                $out['search_console']['rank_tracking']['scheduled_sync'] = !empty($rt['scheduled_sync']);
                $out['search_console']['rank_tracking']['backfill_days'] = $this->clampInt($rt['backfill_days'] ?? $out['search_console']['rank_tracking']['backfill_days'], 1, 30);
                $out['search_console']['rank_tracking']['retention_days'] = $this->clampInt($rt['retention_days'] ?? $out['search_console']['rank_tracking']['retention_days'], 30, 730);

                // Optional query-level tracking.
                if (isset($rt['query_level']) && is_array($rt['query_level'])) {
                    $q = $rt['query_level'];
                    $out['search_console']['rank_tracking']['query_level']['enabled'] = !empty($q['enabled']);
                    $out['search_console']['rank_tracking']['query_level']['row_limit'] = $this->clampInt($q['row_limit'] ?? $out['search_console']['rank_tracking']['query_level']['row_limit'], 1000, 25000);
                    $out['search_console']['rank_tracking']['query_level']['min_impressions'] = $this->clampInt($q['min_impressions'] ?? $out['search_console']['rank_tracking']['query_level']['min_impressions'], 0, 10000000);
                    $out['search_console']['rank_tracking']['query_level']['alert_rows_limit'] = $this->clampInt($q['alert_rows_limit'] ?? $out['search_console']['rank_tracking']['query_level']['alert_rows_limit'], 50, 2000);
                }

                if (isset($rt['alerts']) && is_array($rt['alerts'])) {
                    $a = $rt['alerts'];
                    $out['search_console']['rank_tracking']['alerts']['enabled'] = !empty($a['enabled']);
                    $out['search_console']['rank_tracking']['alerts']['compare_days'] = $this->clampInt($a['compare_days'] ?? $out['search_console']['rank_tracking']['alerts']['compare_days'], 1, 30);
                    $out['search_console']['rank_tracking']['alerts']['min_impressions'] = $this->clampInt($a['min_impressions'] ?? $out['search_console']['rank_tracking']['alerts']['min_impressions'], 0, 10000000);
                    $out['search_console']['rank_tracking']['alerts']['position_drop'] = $this->parseFloat($a['position_drop'] ?? $out['search_console']['rank_tracking']['alerts']['position_drop']);
                    $out['search_console']['rank_tracking']['alerts']['ctr_drop'] = $this->parseFloat($a['ctr_drop'] ?? $out['search_console']['rank_tracking']['alerts']['ctr_drop']);

                    // Optional digest.
                    if (isset($a['digest']) && is_array($a['digest'])) {
                        $d = $a['digest'];
                        $out['search_console']['rank_tracking']['alerts']['digest']['enabled'] = !empty($d['enabled']);
                        $out['search_console']['rank_tracking']['alerts']['digest']['email_enabled'] = !empty($d['email_enabled']);
                        $out['search_console']['rank_tracking']['alerts']['digest']['email_recipients'] = isset($d['email_recipients']) ? sanitize_text_field((string) $d['email_recipients']) : '';
                        $out['search_console']['rank_tracking']['alerts']['digest']['slack_enabled'] = !empty($d['slack_enabled']);
                        $out['search_console']['rank_tracking']['alerts']['digest']['slack_webhook'] = isset($d['slack_webhook']) ? esc_url_raw((string) $d['slack_webhook']) : '';
                        $out['search_console']['rank_tracking']['alerts']['digest']['max_items'] = $this->clampInt($d['max_items'] ?? $out['search_console']['rank_tracking']['alerts']['digest']['max_items'], 1, 100);
                        $out['search_console']['rank_tracking']['alerts']['digest']['include_info'] = !empty($d['include_info']);
                        $out['search_console']['rank_tracking']['alerts']['digest']['include_warning'] = !empty($d['include_warning']);
                        $out['search_console']['rank_tracking']['alerts']['digest']['include_critical'] = !empty($d['include_critical']);

                        // Weekly digest (cron).
                        $out['search_console']['rank_tracking']['alerts']['digest']['weekly_enabled'] = !empty($d['weekly_enabled']);
                        $out['search_console']['rank_tracking']['alerts']['digest']['weekly_weekday'] = $this->clampInt($d['weekly_weekday'] ?? $out['search_console']['rank_tracking']['alerts']['digest']['weekly_weekday'], 1, 7);
                        $out['search_console']['rank_tracking']['alerts']['digest']['weekly_hour_utc'] = $this->clampInt($d['weekly_hour_utc'] ?? $out['search_console']['rank_tracking']['alerts']['digest']['weekly_hour_utc'], 0, 23);
                        $out['search_console']['rank_tracking']['alerts']['digest']['weekly_minute_utc'] = $this->clampInt($d['weekly_minute_utc'] ?? $out['search_console']['rank_tracking']['alerts']['digest']['weekly_minute_utc'], 0, 59);
                        $out['search_console']['rank_tracking']['alerts']['digest']['weekly_days'] = $this->clampInt($d['weekly_days'] ?? $out['search_console']['rank_tracking']['alerts']['digest']['weekly_days'], 1, 30);
                    }

                    // Clamp floats.
                    $pd = (float) $out['search_console']['rank_tracking']['alerts']['position_drop'];
                    if ($pd < 0.5) {
                        $pd = 0.5;
                    }
                    if ($pd > 50.0) {
                        $pd = 50.0;
                    }
                    $out['search_console']['rank_tracking']['alerts']['position_drop'] = $pd;

                    $cd = (float) $out['search_console']['rank_tracking']['alerts']['ctr_drop'];
                    if ($cd < 0.0) {
                        $cd = 0.0;
                    }
                    if ($cd > 1.0) {
                        $cd = 1.0;
                    }
                    $out['search_console']['rank_tracking']['alerts']['ctr_drop'] = $cd;
                }
            }
        }


        // IndexNow
        if (isset($input['indexnow']) && is_array($input['indexnow'])) {
            $in = $input['indexnow'];

            $out['indexnow']['enabled'] = !empty($in['enabled']);
            $out['indexnow']['auto_submit'] = !empty($in['auto_submit']);
            $out['indexnow']['submit_terms'] = !empty($in['submit_terms']);
            $out['indexnow']['submit_redirects'] = !empty($in['submit_redirects']);

            $endpoint = isset($in['endpoint']) ? trim((string) $in['endpoint']) : (string) $out['indexnow']['endpoint'];
            $endpoint = $endpoint !== '' ? esc_url_raw($endpoint) : (string) $out['indexnow']['endpoint'];
            if ($endpoint === '') {
                $endpoint = (string) $out['indexnow']['endpoint'];
            }
            $out['indexnow']['endpoint'] = $endpoint;

            $out['indexnow']['batch_size'] = $this->clampInt($in['batch_size'] ?? $out['indexnow']['batch_size'], 1, 10000);
            $out['indexnow']['dedupe_window'] = $this->clampInt($in['dedupe_window'] ?? $out['indexnow']['dedupe_window'], 0, 3600);
            $out['indexnow']['base_backoff'] = $this->clampInt($in['base_backoff'] ?? $out['indexnow']['base_backoff'], 5, 3600);
            $out['indexnow']['max_backoff'] = $this->clampInt($in['max_backoff'] ?? $out['indexnow']['max_backoff'], 5, 7200);

            // Optional sitemap pings (GET ping endpoints for sitemap URLs).
            if (isset($in['sitemap_pings']) && is_array($in['sitemap_pings'])) {
                $sp = $in['sitemap_pings'];
                $out['indexnow']['sitemap_pings']['enabled'] = !empty($sp['enabled']);
                $out['indexnow']['sitemap_pings']['include_index'] = !empty($sp['include_index']);
                $out['indexnow']['sitemap_pings']['include_news'] = !empty($sp['include_news']);
                $out['indexnow']['sitemap_pings']['include_video'] = !empty($sp['include_video']);

                $prov = isset($sp['providers']) && is_array($sp['providers']) ? $sp['providers'] : [];
                $out['indexnow']['sitemap_pings']['providers'] = [
                    'bing' => !empty($prov['bing']),
                    'google' => !empty($prov['google']),
                ];
            }
        }

        // Privacy & external services
        if (isset($input['privacy']) && is_array($input['privacy'])) {
            $p = $input['privacy'];

            $out['privacy']['trust_mode'] = !empty($p['trust_mode']);
            $out['privacy']['local_only'] = !empty($p['local_only']);
            $out['privacy']['hide_ai_ui'] = !empty($p['hide_ai_ui']);
            $out['privacy']['disable_sitekit_install_helper'] = !empty($p['disable_sitekit_install_helper']);
            $out['privacy']['no_nags'] = !empty($p['no_nags']);
            $out['privacy']['log_external_requests'] = !empty($p['log_external_requests']);
            $out['privacy']['retention_days'] = $this->clampInt(
                $p['retention_days'] ?? ($out['privacy']['retention_days'] ?? 30),
                1,
                365
            );
        }

        // Notifications center
        if (isset($input['notifications']) && is_array($input['notifications'])) {
            $n = $input['notifications'];

            $out['notifications']['enabled'] = !empty($n['enabled']);

            $ms = isset($n['min_severity']) ? sanitize_key((string) $n['min_severity']) : (string) ($out['notifications']['min_severity'] ?? 'warn');
            if (!in_array($ms, ['info', 'warn', 'critical'], true)) {
                $ms = (string) ($out['notifications']['min_severity'] ?? 'warn');
            }
            $out['notifications']['min_severity'] = $ms;

            $out['notifications']['default_mute_days'] = $this->clampInt(
                $n['default_mute_days'] ?? ($out['notifications']['default_mute_days'] ?? 7),
                1,
                365
            );

            if (isset($n['channels']) && is_array($n['channels'])) {
                $c = $n['channels'];
                $out['notifications']['channels']['email_enabled'] = !empty($c['email_enabled']);
                $out['notifications']['channels']['email_recipients'] = isset($c['email_recipients'])
                    ? sanitize_text_field((string) $c['email_recipients'])
                    : (string) ($out['notifications']['channels']['email_recipients'] ?? '');

                $out['notifications']['channels']['slack_enabled'] = !empty($c['slack_enabled']);
                $slack = isset($c['slack_webhook']) ? trim((string) $c['slack_webhook']) : '';
                $slack = $slack !== '' ? esc_url_raw($slack) : '';
                $out['notifications']['channels']['slack_webhook'] = $slack;

                $out['notifications']['channels']['webhook_enabled'] = !empty($c['webhook_enabled']);
                $out['notifications']['channels']['webhook_urls'] = isset($c['webhook_urls'])
                    ? sanitize_textarea_field((string) $c['webhook_urls'])
                    : (string) ($out['notifications']['channels']['webhook_urls'] ?? '');
            }

            if (isset($n['sources']) && is_array($n['sources'])) {
                $s = $n['sources'];
                foreach (['head_diff', 'queue_failed', 'audit_critical', 'quality_gate', 'saas_sync_failed', 'serp_gap_failed', 'platform_ops'] as $k) {
                    $out['notifications']['sources'][$k] = !empty($s[$k]);
                }
            }

            if (isset($n['integrations']) && is_array($n['integrations'])) {
                $ints = $n['integrations'];
                foreach (['drift_monitor', 'gsc_digest'] as $k) {
                    $mode = 'off';
                    if (isset($ints[$k]) && is_array($ints[$k]) && isset($ints[$k]['mode'])) {
                        $mode = sanitize_key((string) $ints[$k]['mode']);
                    } else {
                        $mode = (string) ($out['notifications']['integrations'][$k]['mode'] ?? 'off');
                        $mode = sanitize_key($mode);
                    }

                    if (!in_array($mode, ['off', 'inbox', 'route'], true)) {
                        $mode = 'off';
                    }
                    $out['notifications']['integrations'][$k]['mode'] = $mode;
                }
            }

            if (isset($n['rules']) && is_array($n['rules'])) {
                $r = $n['rules'];

                $out['notifications']['rules']['send_budget_per_hour'] = $this->clampInt(
                    $r['send_budget_per_hour'] ?? ($out['notifications']['rules']['send_budget_per_hour'] ?? 30),
                    0,
                    500
                );

                if (isset($r['sources']) && is_array($r['sources'])) {
                    $rsAll = $r['sources'];
                    foreach (['head_diff', 'queue', 'audit', 'quality_gate', 'saas_sync', 'serp_gap', 'platform_ops', 'drift_monitor', 'search_console', 'test'] as $srcKey) {
                        $rs = isset($rsAll[$srcKey]) && is_array($rsAll[$srcKey]) ? $rsAll[$srcKey] : [];

                        $ms2 = isset($rs['min_severity'])
                            ? sanitize_key((string) $rs['min_severity'])
                            : (string) ($out['notifications']['rules']['sources'][$srcKey]['min_severity'] ?? ($out['notifications']['min_severity'] ?? 'warn'));

                        if (!in_array($ms2, ['info', 'warn', 'critical'], true)) {
                            $ms2 = (string) ($out['notifications']['rules']['sources'][$srcKey]['min_severity'] ?? ($out['notifications']['min_severity'] ?? 'warn'));
                        }
                        $out['notifications']['rules']['sources'][$srcKey]['min_severity'] = $ms2;

                        $out['notifications']['rules']['sources'][$srcKey]['dedupe_window_minutes'] = $this->clampInt(
                            $rs['dedupe_window_minutes'] ?? ($out['notifications']['rules']['sources'][$srcKey]['dedupe_window_minutes'] ?? 0),
                            0,
                            10080
                        );
                    }
                }
            }

            if (isset($n['digests']) && is_array($n['digests'])) {
                $d = $n['digests'];

                $out['notifications']['digests']['daily_enabled'] = !empty($d['daily_enabled']);
                $out['notifications']['digests']['daily_hour_utc'] = $this->clampInt(
                    $d['daily_hour_utc'] ?? ($out['notifications']['digests']['daily_hour_utc'] ?? 9),
                    0,
                    23
                );

                $out['notifications']['digests']['weekly_enabled'] = !empty($d['weekly_enabled']);
                $out['notifications']['digests']['weekly_weekday_utc'] = $this->clampInt(
                    $d['weekly_weekday_utc'] ?? ($out['notifications']['digests']['weekly_weekday_utc'] ?? 1),
                    1,
                    7
                );
                $out['notifications']['digests']['weekly_hour_utc'] = $this->clampInt(
                    $d['weekly_hour_utc'] ?? ($out['notifications']['digests']['weekly_hour_utc'] ?? 9),
                    0,
                    23
                );

                $out['notifications']['digests']['max_items'] = $this->clampInt(
                    $d['max_items'] ?? ($out['notifications']['digests']['max_items'] ?? 25),
                    1,
                    200
                );

                $ms3 = isset($d['min_severity'])
                    ? sanitize_key((string) $d['min_severity'])
                    : (string) ($out['notifications']['digests']['min_severity'] ?? ($out['notifications']['min_severity'] ?? 'warn'));
                if (!in_array($ms3, ['info', 'warn', 'critical'], true)) {
                    $ms3 = (string) ($out['notifications']['digests']['min_severity'] ?? ($out['notifications']['min_severity'] ?? 'warn'));
                }
                $out['notifications']['digests']['min_severity'] = $ms3;

                $out['notifications']['digests']['send_when_empty'] = !empty($d['send_when_empty']);
            }
        }

        // Activity log (admin actions)
        if (isset($input['activity_log']) && is_array($input['activity_log'])) {
            $al = $input['activity_log'];
            $out['activity_log']['retention_days'] = $this->clampInt(
                $al['retention_days'] ?? ($out['activity_log']['retention_days'] ?? 90),
                0,
                3650
            );
        }

        // Audit history (scan runs)
        if (isset($input['audit']) && is_array($input['audit'])) {
            $a = $input['audit'];
            $out['audit']['history_keep_scans'] = $this->clampInt(
                $a['history_keep_scans'] ?? ($out['audit']['history_keep_scans'] ?? 20),
                0,
                500
            );

            // Audit automation (optional scheduled scans).
            if (isset($a['automation']) && is_array($a['automation'])) {
                $au = $a['automation'];

                $schedule = isset($au['schedule']) ? sanitize_key((string) $au['schedule']) : (string) ($out['audit']['automation']['schedule'] ?? 'off');
                if (!in_array($schedule, ['off', 'daily', 'weekly'], true)) {
                    $schedule = (string) ($out['audit']['automation']['schedule'] ?? 'off');
                }

                $out['audit']['automation']['schedule'] = $schedule;
                $out['audit']['automation']['hour_utc'] = $this->clampInt(
                    $au['hour_utc'] ?? ($out['audit']['automation']['hour_utc'] ?? 4),
                    0,
                    23
                );
                $out['audit']['automation']['weekday_utc'] = $this->clampInt(
                    $au['weekday_utc'] ?? ($out['audit']['automation']['weekday_utc'] ?? 1),
                    0,
                    6
                );

                $out['audit']['automation']['include_terms'] = !empty($au['include_terms']);
                $out['audit']['automation']['check_social_image'] = !empty($au['check_social_image']);
            }
        }

        // Config Sync / Agency
        if (isset($input['config_sync']) && is_array($input['config_sync'])) {
            $cs = $input['config_sync'];
            $out['config_sync']['lock_settings'] = !empty($cs['lock_settings']);
        }


        // White label (agency)
        if (isset($input['white_label']) && is_array($input['white_label'])) {
            $wl = $input['white_label'];

            $out['white_label']['enabled'] = !empty($wl['enabled']);
            $out['white_label']['brand_name'] = isset($wl['brand_name']) ? sanitize_text_field((string) $wl['brand_name']) : '';
            $out['white_label']['menu_title'] = isset($wl['menu_title']) ? sanitize_text_field((string) $wl['menu_title']) : '';
            $out['white_label']['settings_heading'] = isset($wl['settings_heading']) ? sanitize_text_field((string) $wl['settings_heading']) : '';
            $out['white_label']['hide_branding'] = !empty($wl['hide_branding']);
            $out['white_label']['hide_upgrade_ui'] = !empty($wl['hide_upgrade_ui']);

            // Clamp lengths.
            foreach (['brand_name','menu_title','settings_heading'] as $k) {
                $v = isset($out['white_label'][$k]) ? (string) $out['white_label'][$k] : '';
                if (strlen($v) > 80) {
                    $out['white_label'][$k] = substr($v, 0, 80);
                }
            }
        }

        // Integrations (Elementor, Divi, Beaver, Custom Fields).
        if (isset($input['integrations']) && is_array($input['integrations'])) {
            $ig = $input['integrations'];

            if (isset($ig['elementor']) && is_array($ig['elementor'])) {
                $el = $ig['elementor'];
                $out['integrations']['elementor']['enabled'] = !empty($el['enabled']);
                $out['integrations']['elementor']['editor_controls'] = !empty($el['editor_controls']);
            }

            if (isset($ig['divi']) && is_array($ig['divi'])) {
                $dv = $ig['divi'];
                $out['integrations']['divi']['enabled'] = !empty($dv['enabled']);
                $out['integrations']['divi']['universal_metabox'] = !empty($dv['universal_metabox']);
            }

            if (isset($ig['beaver']) && is_array($ig['beaver'])) {
                $bb = $ig['beaver'];
                $out['integrations']['beaver']['enabled'] = !empty($bb['enabled']);
                $out['integrations']['beaver']['universal_metabox'] = !empty($bb['universal_metabox']);
            }

            if (isset($ig['custom_fields']) && is_array($ig['custom_fields'])) {
                $cf = $ig['custom_fields'];
                $out['integrations']['custom_fields']['enabled'] = !empty($cf['enabled']);
                $out['integrations']['custom_fields']['include_in_analysis'] = !empty($cf['include_in_analysis']);

                $keys = isset($cf['analysis_keys']) ? sanitize_textarea_field((string) $cf['analysis_keys']) : '';
                $out['integrations']['custom_fields']['analysis_keys'] = trim($keys);

                $max = isset($cf['max_value_chars']) ? (int) $cf['max_value_chars'] : (int) $out['integrations']['custom_fields']['max_value_chars'];
                if ($max < 50) {
                    $max = 50;
                }
                if ($max > 5000) {
                    $max = 5000;
                }
                $out['integrations']['custom_fields']['max_value_chars'] = $max;
            }
        }

        // Performance budget (background processing).
        if (isset($input['performance']) && is_array($input['performance'])) {
            $perf = $input['performance'];

            if (isset($perf['queue']) && is_array($perf['queue'])) {
                $q = $perf['queue'];

                $mode = isset($q['mode']) ? sanitize_key((string) $q['mode']) : (string) ($out['performance']['queue']['mode'] ?? 'auto');
                // auto  : WP-Cron scheduled ticks
                // direct: run worker at the end of the current request (no schedule)
                // manual: no schedule and no direct worker; rely on WP-CLI/tools pages
                $out['performance']['queue']['mode'] = in_array($mode, ['auto', 'direct', 'manual'], true) ? $mode : 'auto';

                $mins = isset($q['interval_minutes']) ? (int) $q['interval_minutes'] : (int) ($out['performance']['queue']['interval_minutes'] ?? 1);
                $allowedMins = [1, 2, 5, 10, 15];
                if (!in_array($mins, $allowedMins, true)) {
                    $mins = (int) ($out['performance']['queue']['interval_minutes'] ?? 1);
                }
                $out['performance']['queue']['interval_minutes'] = $mins;

                $out['performance']['queue']['max_jobs'] = $this->clampInt($q['max_jobs'] ?? ($out['performance']['queue']['max_jobs'] ?? 5), 1, 50);

                // Direct mode tuning.
                if (isset($q['direct']) && is_array($q['direct'])) {
                    $d = $q['direct'];
                    $out['performance']['queue']['direct']['max_jobs'] = $this->clampInt(
                        $d['max_jobs'] ?? ($out['performance']['queue']['direct']['max_jobs'] ?? 200),
                        1,
                        5000
                    );
                    $out['performance']['queue']['direct']['max_seconds'] = $this->clampInt(
                        $d['max_seconds'] ?? ($out['performance']['queue']['direct']['max_seconds'] ?? 20),
                        1,
                        180
                    );
                    $out['performance']['queue']['direct']['eager_run_at_seconds'] = $this->clampInt(
                        $d['eager_run_at_seconds'] ?? ($out['performance']['queue']['direct']['eager_run_at_seconds'] ?? 5),
                        0,
                        60
                    );
                }


                // Auto-pause on 503/429 bursts.
                if (isset($q['autopause']) && is_array($q['autopause'])) {
                    $ap = $q['autopause'];
                    $out['performance']['queue']['autopause']['enabled'] = !empty($ap['enabled']);

                    $out['performance']['queue']['autopause']['threshold'] = $this->clampInt(
                        $ap['threshold'] ?? ($out['performance']['queue']['autopause']['threshold'] ?? 3),
                        2,
                        50
                    );

                    $out['performance']['queue']['autopause']['window_minutes'] = $this->clampInt(
                        $ap['window_minutes'] ?? ($out['performance']['queue']['autopause']['window_minutes'] ?? 10),
                        1,
                        180
                    );

                    $out['performance']['queue']['autopause']['pause_minutes'] = $this->clampInt(
                        $ap['pause_minutes'] ?? ($out['performance']['queue']['autopause']['pause_minutes'] ?? 30),
                        1,
                        7 * 24 * 60
                    );
                }
            }

            if (isset($perf['admin']) && is_array($perf['admin'])) {
                $a = $perf['admin'];
                $out['performance']['admin']['minimize_admin_footprint'] = !empty($a['minimize_admin_footprint']);
                $out['performance']['admin']['collect_metrics'] = !empty($a['collect_metrics']);
                $out['performance']['admin']['record_history'] = !empty($a['record_history']);
                $out['performance']['admin']['ajax_enabled'] = !empty($a['ajax_enabled']);
                $out['performance']['admin']['ajax_lite_boot'] = !empty($a['ajax_lite_boot']);
            }

            // Frontend performance contract.
            if (isset($perf['frontend']) && is_array($perf['frontend'])) {
                $f = $perf['frontend'];
                $out['performance']['frontend']['contract_enabled'] = !empty($f['contract_enabled']);
                $out['performance']['frontend']['inspector_on_demand'] = !empty($f['inspector_on_demand']);
            }
        }

        // Drift Monitor.
        if (isset($input['drift_monitor']) && is_array($input['drift_monitor'])) {
            $dm = $input['drift_monitor'];
            $out['drift_monitor']['enabled'] = !empty($dm['enabled']);
            $out['drift_monitor']['seed_gsc_limit'] = $this->clampInt(
                $dm['seed_gsc_limit'] ?? ($out['drift_monitor']['seed_gsc_limit'] ?? 20),
                1,
                200
            );

            // GSC drift rules.
            if (isset($dm['gsc']) && is_array($dm['gsc'])) {
                $g = $dm['gsc'];
                $out['drift_monitor']['gsc']['enabled'] = !empty($g['enabled']);
                $out['drift_monitor']['gsc']['auto_track_top_pages'] = !empty($g['auto_track_top_pages']);
                $out['drift_monitor']['gsc']['window_days'] = $this->clampInt(
                    $g['window_days'] ?? ($out['drift_monitor']['gsc']['window_days'] ?? 7),
                    1,
                    28
                );
                $out['drift_monitor']['gsc']['min_impressions'] = $this->clampInt(
                    $g['min_impressions'] ?? ($out['drift_monitor']['gsc']['min_impressions'] ?? 100),
                    0,
                    1000000
                );

                $out['drift_monitor']['gsc']['ctr_drop_warn_pct'] = $this->clampFloat(
                    $g['ctr_drop_warn_pct'] ?? ($out['drift_monitor']['gsc']['ctr_drop_warn_pct'] ?? 2.0),
                    0.0,
                    100.0
                );
                $out['drift_monitor']['gsc']['ctr_drop_critical_pct'] = $this->clampFloat(
                    $g['ctr_drop_critical_pct'] ?? ($out['drift_monitor']['gsc']['ctr_drop_critical_pct'] ?? 5.0),
                    0.0,
                    100.0
                );
                $out['drift_monitor']['gsc']['position_drop_warn'] = $this->clampFloat(
                    $g['position_drop_warn'] ?? ($out['drift_monitor']['gsc']['position_drop_warn'] ?? 2.0),
                    0.0,
                    1000.0
                );
                $out['drift_monitor']['gsc']['position_drop_critical'] = $this->clampFloat(
                    $g['position_drop_critical'] ?? ($out['drift_monitor']['gsc']['position_drop_critical'] ?? 5.0),
                    0.0,
                    1000.0
                );
            }

            // Notifications.
            if (isset($dm['notifications']) && is_array($dm['notifications'])) {
                $n = $dm['notifications'];
                $out['drift_monitor']['notifications']['enabled'] = !empty($n['enabled']);
                $minSev = isset($n['min_severity']) ? sanitize_key((string) $n['min_severity']) : (string) ($out['drift_monitor']['notifications']['min_severity'] ?? 'warn');
                $out['drift_monitor']['notifications']['min_severity'] = in_array($minSev, ['info', 'warn', 'critical'], true) ? $minSev : 'warn';
                $out['drift_monitor']['notifications']['mute_days'] = $this->clampInt(
                    $n['mute_days'] ?? ($out['drift_monitor']['notifications']['mute_days'] ?? 7),
                    0,
                    365
                );

                $out['drift_monitor']['notifications']['email_enabled'] = !empty($n['email_enabled']);
                $out['drift_monitor']['notifications']['email_recipients'] = sanitize_text_field((string) ($n['email_recipients'] ?? ($out['drift_monitor']['notifications']['email_recipients'] ?? '')));

                $out['drift_monitor']['notifications']['slack_enabled'] = !empty($n['slack_enabled']);
                $out['drift_monitor']['notifications']['slack_webhook'] = esc_url_raw((string) ($n['slack_webhook'] ?? ($out['drift_monitor']['notifications']['slack_webhook'] ?? '')));

                $out['drift_monitor']['notifications']['webhook_enabled'] = !empty($n['webhook_enabled']);

                // Keep raw, but trim and normalize line endings.
                $wh = (string) ($n['webhook_urls'] ?? ($out['drift_monitor']['notifications']['webhook_urls'] ?? ''));
                $wh = str_replace(["\r\n", "\r"], "\n", $wh);
                $out['drift_monitor']['notifications']['webhook_urls'] = trim($wh);
            }
        }

        // Site Crawl Analyzer (optional automation/settings)
        if (isset($input['site_crawl']) && is_array($input['site_crawl'])) {
            $sc = $input['site_crawl'];

            if (isset($sc['automation']) && is_array($sc['automation'])) {
                $a = $sc['automation'];
                $schedule = isset($a['schedule']) ? sanitize_key((string) $a['schedule']) : (string) ($out['site_crawl']['automation']['schedule'] ?? 'off');
                $out['site_crawl']['automation']['schedule'] = in_array($schedule, ['off', 'daily', 'weekly'], true) ? $schedule : (string) ($out['site_crawl']['automation']['schedule'] ?? 'off');

                $out['site_crawl']['automation']['hour_utc'] = $this->clampInt(
                    $a['hour_utc'] ?? ($out['site_crawl']['automation']['hour_utc'] ?? 3),
                    0,
                    23
                );
                $out['site_crawl']['automation']['weekday_utc'] = $this->clampInt(
                    $a['weekday_utc'] ?? ($out['site_crawl']['automation']['weekday_utc'] ?? 1),
                    0,
                    6
                );
                $out['site_crawl']['automation']['retention_runs'] = $this->clampInt(
                    $a['retention_runs'] ?? ($out['site_crawl']['automation']['retention_runs'] ?? 30),
                    0,
                    500
                );

                if (isset($a['opts']) && is_array($a['opts'])) {
                    $o = $a['opts'];
                    $out['site_crawl']['automation']['opts']['max_urls'] = $this->clampInt(
                        $o['max_urls'] ?? ($out['site_crawl']['automation']['opts']['max_urls'] ?? 2000),
                        100,
                        200000
                    );
                    $out['site_crawl']['automation']['opts']['max_depth'] = $this->clampInt(
                        $o['max_depth'] ?? ($out['site_crawl']['automation']['opts']['max_depth'] ?? 8),
                        1,
                        50
                    );
                    $out['site_crawl']['automation']['opts']['respect_robots'] = !empty($o['respect_robots']);
                    $out['site_crawl']['automation']['opts']['include_sitemaps'] = !empty($o['include_sitemaps']);
                    $out['site_crawl']['automation']['opts']['include_query'] = !empty($o['include_query']);
                }
            }

            if (isset($sc['regression_alerts']) && is_array($sc['regression_alerts'])) {
                $r = $sc['regression_alerts'];
                $out['site_crawl']['regression_alerts']['enabled'] = !empty($r['enabled']);
                foreach (['delta_errors', 'delta_5xx', 'delta_canonical_mismatch', 'delta_missing_meta', 'delta_dupe_canonical', 'delta_noindex'] as $k) {
                    $out['site_crawl']['regression_alerts'][$k] = $this->clampInt(
                        $r[$k] ?? ($out['site_crawl']['regression_alerts'][$k] ?? 0),
                        0,
                        100000
                    );
                }

                // Optional message enrichment.
                $out['site_crawl']['regression_alerts']['sample_urls'] = $this->clampInt(
                    $r['sample_urls'] ?? ($out['site_crawl']['regression_alerts']['sample_urls'] ?? 5),
                    0,
                    20
                );
                $out['site_crawl']['regression_alerts']['include_samples_in_message'] = isset($r['include_samples_in_message'])
                    ? !empty($r['include_samples_in_message'])
                    : (bool) ($out['site_crawl']['regression_alerts']['include_samples_in_message'] ?? true);
            }
        }

        // Quality Gate (optional automation/settings)
        if (isset($input['quality_gate']) && is_array($input['quality_gate'])) {
            $qg = $input['quality_gate'];
            if (isset($qg['automation']) && is_array($qg['automation'])) {
                $a = $qg['automation'];
                $schedule = isset($a['schedule']) ? sanitize_key((string) $a['schedule']) : (string) ($out['quality_gate']['automation']['schedule'] ?? 'off');
                $out['quality_gate']['automation']['schedule'] = in_array($schedule, ['off', 'daily', 'weekly'], true) ? $schedule : (string) ($out['quality_gate']['automation']['schedule'] ?? 'off');

                $out['quality_gate']['automation']['hour_utc'] = $this->clampInt(
                    $a['hour_utc'] ?? ($out['quality_gate']['automation']['hour_utc'] ?? 4),
                    0,
                    23
                );
                $out['quality_gate']['automation']['weekday_utc'] = $this->clampInt(
                    $a['weekday_utc'] ?? ($out['quality_gate']['automation']['weekday_utc'] ?? 1),
                    0,
                    6
                );
            }
        }

        // Platform Ops (optional automation/settings)
        if (isset($input['platform_ops']) && is_array($input['platform_ops'])) {
            $po = $input['platform_ops'];
            if (isset($po['automation']) && is_array($po['automation'])) {
                $a = $po['automation'];
                $schedule = isset($a['schedule']) ? sanitize_key((string) $a['schedule']) : (string) ($out['platform_ops']['automation']['schedule'] ?? 'off');
                $out['platform_ops']['automation']['schedule'] = in_array($schedule, ['off', 'daily', 'weekly'], true) ? $schedule : (string) ($out['platform_ops']['automation']['schedule'] ?? 'off');
                $out['platform_ops']['automation']['configured'] = isset($a['configured'])
                    ? !empty($a['configured'])
                    : (bool) ($out['platform_ops']['automation']['configured'] ?? false);
                $out['platform_ops']['automation']['hour_utc'] = $this->clampInt(
                    $a['hour_utc'] ?? ($out['platform_ops']['automation']['hour_utc'] ?? 6),
                    0,
                    23
                );
                $out['platform_ops']['automation']['weekday_utc'] = $this->clampInt(
                    $a['weekday_utc'] ?? ($out['platform_ops']['automation']['weekday_utc'] ?? 1),
                    0,
                    6
                );
            }
        }

        // Compatibility & recovery modes.
        if (isset($input['compatibility']) && is_array($input['compatibility'])) {
            $c = $input['compatibility'];
            $out['compatibility']['safe_mode'] = !empty($c['safe_mode']);
            $out['compatibility']['disable_output'] = !empty($c['disable_output']);
            $out['compatibility']['auto_disable_output_on_conflict'] = !empty($c['auto_disable_output_on_conflict']);
        }



// modules: array of moduleId => bool
        $out['modules'] = [];
        if (isset($input['modules']) && is_array($input['modules'])) {
            foreach ($input['modules'] as $moduleId => $enabled) {
                $moduleId = sanitize_key((string) $moduleId);
                $out['modules'][$moduleId] = (bool) $enabled;
            }
        }

        // If settings UI is locked, always keep Config Sync module enabled to avoid lock-out.
        if (!empty($out['config_sync']['lock_settings'])) {
            $out['modules']['config_sync'] = true;
        }

        return $out;
    }

    private function sanitizeRobotsTxtCustom(string $text): string
    {
        $text = trim($text);
        if ($text === '') {
            return '';
        }

        $lines = preg_split('/\r\n|\r|\n/', $text) ?: [];
        $out = [];

        $directiveMap = [
            'user-agent'  => 'User-agent',
            'disallow'    => 'Disallow',
            'allow'       => 'Allow',
            'sitemap'     => 'Sitemap',
            'crawl-delay' => 'Crawl-delay',
            'host'        => 'Host',
        ];

        foreach ($lines as $line) {
            if (count($out) > 300) {
                break; // safety limit
            }
            $line = trim((string) $line);
            if ($line === '') {
                continue;
            }

            if (strpos($line, '#') === 0) {
                $out[] = '#' . ltrim(substr($line, 1));
                continue;
            }

            if (!preg_match('/^\s*([A-Za-z\-]+)\s*:\s*(.*)$/', $line, $m)) {
                continue;
            }

            $dir = strtolower((string) $m[1]);
            $val = (string) $m[2];

            if (!isset($directiveMap[$dir])) {
                continue;
            }

            $dirOut = $directiveMap[$dir];

            if ($dir === 'sitemap') {
                $val = esc_url_raw(trim($val));
                if ($val === '') {
                    continue;
                }
            } elseif ($dir === 'crawl-delay') {
                $val = trim($val);
                if ($val === '' || !is_numeric($val)) {
                    continue;
                }
                $val = (string) (int) $val;
            } else {
                // Keep it readable; allow common robots characters.
                $val = sanitize_text_field($val);
            }

            $out[] = $dirOut . ': ' . $val;
        }

        return trim(implode("\n", $out));
    }

    private function clampInt($value, int $min, int $max): int
    {
        $v = (int) $value;
        if ($v < $min) {
            $v = $min;
        }
        if ($v > $max) {
            $v = $max;
        }
        return $v;
    }

    private function sanitizeSearchUrlBlacklistPatterns(string $raw): string
    {
        $lines = preg_split('/\r\n|\r|\n/', $raw) ?: [];
        $clean = [];
        $seen = [];

        foreach ($lines as $line) {
            $line = preg_replace('/[\x00-\x1F\x7F]+/u', '', (string) $line) ?? (string) $line;
            $line = trim($line);
            if ($line === '') {
                continue;
            }

            if ($this->stringLengthUtf8($line) > 200) {
                $line = $this->substringUtf8($line, 200);
                $line = trim($line);
            }

            if ($line === '') {
                continue;
            }

            $key = strtolower($line);
            if (isset($seen[$key])) {
                continue;
            }

            if ($this->normalizeRegexPatternForValidation($line) === '') {
                continue;
            }

            $seen[$key] = true;
            $clean[] = $line;

            if (count($clean) >= 30) {
                break;
            }
        }

        return implode("\n", $clean);
    }

    private function normalizeRegexPatternForValidation(string $pattern): string
    {
        $pattern = trim($pattern);
        if ($pattern === '') {
            return '';
        }

        if (preg_match('/^(.)(?:.*)\1[imsxuADSUXJ]*$/', $pattern) === 1) {
            $ok = @preg_match($pattern, '');
            return $ok === false ? '' : $pattern;
        }

        $regex = '~' . str_replace('~', '\\~', $pattern) . '~iu';
        $ok = @preg_match($regex, '');

        return $ok === false ? '' : $regex;
    }

    private function stringLengthUtf8(string $value): int
    {
        if (function_exists('mb_strlen')) {
            return (int) mb_strlen($value, 'UTF-8');
        }

        return strlen($value);
    }

    private function substringUtf8(string $value, int $limit): string
    {
        if ($limit < 1) {
            return '';
        }

        if (function_exists('mb_substr')) {
            return (string) mb_substr($value, 0, $limit, 'UTF-8');
        }

        return substr($value, 0, $limit);
    }

    private function clampFloat($value, float $min, float $max): float
    {
        $v = $this->parseFloat($value);
        if ($v < $min) {
            $v = $min;
        }
        if ($v > $max) {
            $v = $max;
        }
        return $v;
    }

    private function parseFloat($value): float
    {
        if (is_string($value)) {
            $value = str_replace(',', '.', $value);
        }
        return (float) $value;
    }

    private function sanitizeWebhookUrl(string $url): string
    {
        $url = trim($url);
        if ($url === '') {
            return '';
        }

        $url = esc_url_raw($url);
        if ($url === '') {
            return '';
        }

        // Only allow http(s).
        if (function_exists('wp_parse_url')) {
            $parts = wp_parse_url($url);
            if (!is_array($parts) || empty($parts['scheme'])) {
                return '';
            }
            $scheme = strtolower((string) $parts['scheme']);
            if (!in_array($scheme, ['http', 'https'], true)) {
                return '';
            }
        }

        return $url;
    }

    /**
     * @return array<string, mixed>
     */

    private function sanitizeMaybeRelativeUrl(string $url): string
    {
        $url = trim($url);
        if ($url === '') {
            return '';
        }

        if (strpos($url, '/') === 0 && strpos($url, '//') !== 0) {
            $url = home_url($url);
        }

        return esc_url_raw($url);
    }

    public function defaults(): array
    {
        $paidBuild = PlanMatrix::buildTier() !== PlanMatrix::TIER_FREE;

        return [
            'debug'     => false,
            'separator' => '–',
            'ui' => [
                // Default to the full UI so the whole plugin is visible out of the box.
                // Users can still switch to Simple mode if they prefer a cleaner menu.
                'mode' => 'pro',
            ],
            'templates' => [
                'title'             => '%title% %sep% %sitename%',
                'description'       => '%excerpt|strip|truncate(160)%',
                'author_title'      => '%author_name% %sep% %sitename%',
                'author_description' => '%author_bio%',
            ],

            // Verification meta tags for webmaster tools.
            // Values are stored as plain tokens (not full meta tags).
            'verification' => [
                'google' => '',
                'bing' => '',
                'yandex' => '',
                'baidu' => '',
                'ahrefs' => '',
            ],
            'robots'    => [
                'contexts' => [
                    'home'     => ['index' => true, 'follow' => true],
                    'singular' => ['index' => true, 'follow' => true],
                    'term'     => ['index' => true, 'follow' => true],
                    'archive'  => ['index' => true, 'follow' => true],
                    'search'   => ['index' => false, 'follow' => true],
                    '404'      => ['index' => false, 'follow' => true],
                ],
                'noindex_paged' => false,
                'url_noindex_rules' => [],
                'advanced' => [
                    // Keep WP's default behavior (it already adds max-image-preview:large).
                    'max-image-preview' => 'large',
                    'max-snippet'       => '-1',
                    'max-video-preview' => '-1',
                    'noarchive'         => false,
                    'nosnippet'         => false,
                ],
            ],
            'archive_controls' => [
                'author' => [
                    'enabled' => false,
                    'mode' => 'index',
                    'index' => true,
                    'follow' => true,
                    'redirect_url' => '',
                ],
                'date' => [
                    'enabled' => false,
                    'mode' => 'index',
                    'index' => true,
                    'follow' => true,
                    'redirect_url' => '',
                ],
                'post_type' => [
                    'enabled' => false,
                    'mode' => 'index',
                    'index' => true,
                    'follow' => true,
                    'redirect_url' => '',
                ],
                'attachments' => [
                    'mode' => 'default',
                ],
            ],

            'schema'   => [
                // Organization representation (optional overrides).
                'org_name'     => '',
                'org_alternate_name' => '',
                'org_description' => '',
                'org_email' => '',
                'org_telephone' => '',
                'org_legal_name' => '',
                'org_founding_date' => '',
                'org_number_employees' => 0,
                'org_vat_id' => '',
                'org_tax_id' => '',
                'org_iso6523_code' => '',
                'org_duns' => '',
                'org_lei_code' => '',
                'org_naics' => '',
                'org_logo_url' => '',
                // array of URLs
                'org_same_as'  => [],
                'policy_urls' => [
                    'publishing_principles' => '',
                    'ownership_funding_info' => '',
                    'actionable_feedback_policy' => '',
                    'corrections_policy' => '',
                    'ethics_policy' => '',
                    'diversity_policy' => '',
                    'diversity_staffing_report' => '',
                ],
                'default_types' => [],
                'custom_json_ld' => [
                    // Optional JSON-LD templates keyed by post type, e.g. ['post' => '{...}'].
                    'post_type_templates' => [],
                ],
            ],
            'social'  => [
                'default_image_url' => '',
                'fallback_author_image' => false,
            ],

            'hreflang' => [
                'enabled' => false,
                'include_x_default' => false,
                'x_default_url' => '',
            ],

            // Licensing / entitlements (optional)
            'licensing' => [
                'server_url' => '',
                'verify_path' => '/wp-seo-next/license/verify',
                'public_key_pem' => '',
            // Optional plugin update endpoint (Pro builds).
            'update_server_url' => '',
            'update_path' => '/wp-seo-next/plugin/update',
            'enable_updates' => false,
            // Optional: where "Upgrade" should point.
            'purchase_url' => '',
            // Billing Center inside WP (manual fallbacks; payload can override at runtime).
            'billing_portal_url' => '',
            'account_portal_url' => '',
            'invoices_url' => '',
            'change_plan_url' => '',
            'cancel_url' => '',
            'payment_method_url' => '',
            'billing_support_url' => '',
            'billing_embed' => false,
            'billing_embed_height' => 980,
            'billing_embed_allowed_hosts' => [],
            'billing_sync_enabled' => false,
            'billing_sync_path' => '/wp-seo-next/billing/sync',
            'billing_sync_timeout' => 15,
            'billing_sync_cache_ttl_hours' => 12,
            'billing_sync_scheduled' => true,
            'billing_sync_require_signed' => false,
            'self_serve_enabled' => false,
            'self_serve_catalog_path' => '/wp-seo-next/billing/catalog',
            'self_serve_checkout_path' => '/wp-seo-next/billing/checkout',
            'self_serve_action_path' => '/wp-seo-next/billing/action',
            'self_serve_manage_path' => '/wp-seo-next/billing/manage',
            'self_serve_timeout' => 20,
            'self_serve_cache_ttl_hours' => 6,
            'self_serve_require_signed' => false,
                'cache_ttl_hours' => 24,
                'grace_days' => 14,
                'enforce_entitlements' => $paidBuild,
                'require_signed' => false,
                'scheduled_verify' => true,
            ],

            // SaaS Sync (optional)
            'saas_sync' => [
                'enabled' => false,
                // If empty, Licensing server_url will be used.
                'server_url' => '',
                'sync_path' => '/wp-seo-next/saas/sync',
                'cache_ttl_hours' => 24,
                'grace_days' => 14,
                // Anti-piracy core: prefer signed policies.
                'require_signed' => true,
                'scheduled_sync' => true,
                // hourly|twicedaily|daily|wpsn_15min...
                'schedule' => 'hourly',
                // HTTP timeout for sync.
                'timeout' => 15,
                // Apply restrictive runtime overrides from payload.
                'enforce_remote_toggles' => false,
                // If payload defines allowed providers, enforce it in SERP Gap.
                'enforce_serp_providers' => false,
                // Default SaaS SERP endpoint path (used when payload has no endpoints.serp).
                'serp_path' => '/wp-seo-next/serp/search',
                'serp_timeout' => 20,
                // Deep fetch: competitor page analysis (headings/schema/features).
                'serp_page_path' => '/wp-seo-next/serp/page',
                'serp_page_timeout' => 20,
                // AI proxy: text generation.
                'ai_path' => '/wp-seo-next/ai/generate',
                'ai_timeout' => 60,
                // AI credits: balance lookup.
                'ai_credits_path' => '/wp-seo-next/ai/credits',
                'ai_credits_timeout' => 20,
                // Site-wide crawl analyzer (cloud required).
                'crawl_path' => '/wp-seo-next/crawl',
                'crawl_timeout' => 30,
                // Cloud keyword clustering provider.
                'clustering_path' => '/wp-seo-next/clustering/build',
                'clustering_timeout' => 30,
            ],

            // SaaS Dashboard connector (optional)
            'saas_dashboard' => [
                'enabled' => false,
                'scheduled_sync' => true,
                'schedule' => 'hourly',
                'timeout' => 15,
                // Security: tasks/notes should be signed by default.
                'require_signed_tasks' => true,
                'health_path' => '/wp-seo-next/dashboard/health',
                'tasks_path' => '/wp-seo-next/dashboard/tasks',
                'ack_path' => '/wp-seo-next/dashboard/task/ack',
            ],

            // Site Crawl Analyzer (cloud required)
            'site_crawl' => [
                // Scheduled runs (optional; OFF by default to avoid surprises).
                'automation' => [
                    // off|daily|weekly
                    'schedule' => 'off',
                    // 0-23 UTC
                    'hour_utc' => 3,
                    // 0-6 (Sun..Sat) UTC
                    'weekday_utc' => 1,
                    // Default crawl options for scheduled runs.
                    'opts' => [
                        'max_urls' => 2000,
                        'max_depth' => 8,
                        'respect_robots' => true,
                        'include_sitemaps' => true,
                        'include_query' => false,
                    ],
                    // Keep only the last N runs (0 = keep forever).
                    'retention_runs' => 30,
                ],
                // Regression alerts are generated on run completion by comparing with the previous run.
                'regression_alerts' => [
                    'enabled' => true,
                    // Thresholds (delta vs previous run) to trigger an alert.
                    'delta_errors' => 1,
                    'delta_5xx' => 1,
                    'delta_canonical_mismatch' => 10,
                    'delta_missing_meta' => 10,
                    'delta_dupe_canonical' => 10,
                    'delta_noindex' => 10,

                    // Optional message enrichment.
                    // 0-20 example URLs (best-effort).
                    'sample_urls' => 5,
                    // Include examples in the notification message.
                    'include_samples_in_message' => true,
                ],
            ],

            // Quality Gate (optional automation)
            'quality_gate' => [
                'automation' => [
                    // off|daily|weekly
                    'schedule' => 'off',
                    // 0-23 UTC
                    'hour_utc' => 4,
                    // 0-6 (Sun..Sat) UTC
                    'weekday_utc' => 1,
                ],
            ],

            // Platform Ops (optional automation)
            'platform_ops' => [
                'automation' => [
                    // Explicit UI-controlled schedule. When false, Platform Ops can
                    // still fall back to the legacy notification-driven daily mode
                    // for upgraded installs until the page settings are saved.
                    'configured' => false,
                    // off|daily|weekly
                    'schedule' => 'off',
                    // 0-23 UTC
                    'hour_utc' => 6,
                    // 0-6 (Sun..Sat) UTC
                    'weekday_utc' => 1,
                ],
            ],

            // SERP gap analyzer (optional)
            'serp_gap' => [
                'provider_id' => 'manual',
                'limit' => 5,
                // Default analysis depth for the tools screen.
                // quick = titles/snippets only, deep = fetch competitor headings via queue.
                'depth' => 'quick',
                'deep' => [
                    // Competitor page fetch timeout (seconds).
                    'timeout' => 8,
                    // Hard cap to avoid huge HTML payloads.
                    'max_html_bytes' => 2000000,
                ],
                'serper' => [
                    'endpoint' => 'https://google.serper.dev/search',
                    // Optional defaults (ISO-3166 country code / language code).
                    'gl' => '',
                    'hl' => '',
                    // How many organic results to request from the API (we then take Top N limit).
                    'num' => 10,
                    // Best-effort plugin limiter. 0 = unlimited.
                    'rpm_limit' => 60,
                    // Fallback backoff seconds when HTTP 429 has no Retry-After.
                    'backoff_seconds' => 30,
                    // HTTP timeout.
                    'timeout' => 15,
                ],
            ],

            // Breadcrumbs (frontend UI)
            'breadcrumbs' => [
                // Visual breadcrumbs UI output (shortcode + auto-insert).
                'ui_enabled' => true,
                // BreadcrumbList schema node in JSON-LD.
                'schema_enabled' => true,
                // Optional custom home label.
                'home_label' => '',
                // Optional text shown before the trail, e.g. "You are here:".
                'trail_prefix' => '',
                // Optional prefix for archive breadcrumb labels.
                'archive_prefix' => '',
                // Optional prefix for search breadcrumb labels.
                'search_prefix' => '',
                // Optional custom label for the 404 breadcrumb.
                'not_found_label' => '',
                // Insert automatically into the_content.
                'auto_insert' => false,
                // before|after
                'position' => 'before',
                'separator' => '›',
                'show_current' => true,
                // Only insert on singular pages (posts/pages/CPT).
                'singular_only' => true,
            ],

            // WooCommerce integration (optional)
            'woocommerce' => [
                'schema' => [
                    // Enable Product schema output for WooCommerce products.
                    'enabled' => true,
                    // Prevent duplicate schema by disabling WooCommerce native structured data.
                    'disable_wc_structured_data' => true,
                ],
                'social' => [
                    // Output product OpenGraph meta (price/currency/availability) and set og:type=product.
                    'enabled' => true,
                    'set_og_type_product' => true,
                    'include_price' => true,
                    'include_availability' => true,
                ],

                // Canonical & robots helpers for shop/category filter URLs.
                // NOTE: We only remove a safe set of known WooCommerce filter/query parameters.
                'canonical' => [
                    // Strip layered nav / filter parameters from canonical URLs.
                    'strip_filter_args' => true,
                    // Optional: noindex filtered shop/category pages when filter params are present.
                    'noindex_filtered_archives' => false,
                    // Extra query args to strip from canonical on WooCommerce pages (comma-separated).
                    'extra_strip_args' => '',
                ],

                // Google Merchant / Shopping feed (RSS 2.0 + g: namespace)
                // Endpoint: /merchant-feed.xml
                'merchant_feed' => [
                    'enabled' => false,
                    // If enabled, output each variation as its own item and set g:item_group_id.
                    'include_variations' => false,
                    // Only include products/variations in stock.
                    'in_stock_only' => false,
                    // Exclude catalog-hidden products.
                    'exclude_hidden' => true,
                    // Query chunk size when generating the feed.
                    'posts_per_page' => 200,
                    // 0 = unlimited.
                    'max_products' => 0,
                    // new|used|refurbished
                    'condition' => 'new',
                    // 0 = no transient caching (recommended on large sites without object cache).
                    'cache_ttl_sec' => 0,
                ],
            ],

            // Local SEO (optional)
            'local' => [
                'schema' => [
                    'enabled' => true,
                    // Add LocalBusiness graph on the home page as well.
                    'show_on_home' => false,
                    // Schema.org business type (e.g. LocalBusiness, Restaurant, Dentist)
                    'business_type' => 'LocalBusiness',
                ],
                // Optional: primary location (Location post ID). Used for home-page schema + NAP defaults.
                'primary_location_id' => 0,

                // NAP rendering defaults (shortcode / block).
                'nap' => [
                    // plain | multiline | icons | icons_multiline
                    'template' => 'plain',
                    // Optional HTML microdata in addition to JSON-LD.
                    'microdata' => false,
                ],
                // KML feed of locations.
                // Endpoint: /locations.kml
                'kml' => [
                    'enabled' => true,
                ],
                // Special sitemap for locations.
                // Endpoint: /locations-sitemap.xml (sitemap slug: locations)
                'locations_sitemap' => [
                    'enabled' => true,
                ],
                // Primary business info (used as defaults for schema + audit).
                'business' => [
                    'name' => '',
                    'telephone' => '',
                    'email' => '',
                    'street_address' => '',
                    'address_locality' => '',
                    'address_region' => '',
                    'postal_code' => '',
                    'address_country' => '',
                    'price_range' => '',
                    // string decimals
                    'latitude' => '',
                    'longitude' => '',
                    // one per line (free text)
                    'opening_hours' => '',
                ],
            ],
            'media' => [
                'filenames' => [
                    // Filename dirty detector config (used by Media filename cleanup drafts).
                    'dirty_groups' => [
                        // IMG_1234 / DSC0001 / PXL_... / image-123
                        'camera' => true,
                        // screenshot-2024-...
                        'screenshot' => true,
                        // whatsapp-image / telegram-image / signal-...
                        'messenger' => true,
                        // copy-of / edited / cropped / resized / pasted-image
                        'editor' => true,
                        // untitled / scan
                        'untitled' => true,
                        // mostly-numeric / timestamps
                        'numeric' => true,
                        // hash-like names
                        'hash' => true,
                    ],
                    // One prefix per line, e.g. "banner" (matches banner-123.jpg)
                    'custom_prefixes' => '',
                ],
            ],
            'sitemap'  => [
                'urls_per_sitemap'     => 1000,
                'include_taxonomies'   => true,
                // Maps: slug => bool. Empty = all public.
                'post_types'           => [],
                'taxonomies'           => [],

                // Optional image sitemap (adds <image:image> entries to post sitemaps).
                'images' => [
                    'enabled' => false,
                    'max_per_url' => 5,
                    'include_featured' => true,
                    'include_content' => true,
                ],

                // Optional Google News sitemap (adds <news:news> entries for recently published posts).
                // URL: /news-sitemap.xml
                'news' => [
                    'enabled' => false,
                    // If empty, falls back to site name.
                    'publication_name' => '',
                    // If empty, falls back to site locale (e.g., en).
                    'language' => '',
                    // Optional Google News genres value.
                    'genres' => '',
                    // Include <news:keywords> from categories/tags.
                    'include_keywords' => true,
                    // Maps: post type slug => bool (default: only "post").
                    'post_types' => [
                        'post' => true,
                    ],
                ],

                // Optional Video sitemap (adds <video:video> entries for posts with video meta).
                // URL: /video-sitemap.xml
                'video' => [
                    'enabled' => false,
                    // meta | youtube_auto
                    'mode' => 'meta',
                    'include_featured_thumbnail' => true,
                    // Maps: post type slug => bool. Empty = use general sitemap.post_types.
                    'post_types' => [],
                ],
            ],
            'robots_txt' => [
                'append_sitemap' => true,
                'custom'         => '',

                // Structured list of User-agent blocks managed from the Robots screen.
                'agent_groups' => [],

                // Structured list of managed rules (Allow/Disallow).
                // These are appended as a separate "User-agent: *" section.
                'disallow_rules' => [],
            ],
            'search_urls' => [
                'enabled' => false,
                'strip_category_base' => false,
                'mode' => 'format',
                'preserve_args' => '',
                'sanitize_query' => false,
                'query_limit' => 50,
                'strip_emoji' => false,
                'block_common_patterns' => false,
                'blacklist_patterns' => '',
                'invalid_action' => '404',
            ],
            'head_cleanup' => HeadCleanupProfile::defaultConfig(),
            'ai' => [
                'local' => [
                    'enabled' => true,
                    'title_max_length' => 60,
                    'description_max_length' => 155,
                    'keywords_limit' => 12,
                    // used for external prompts as well
                    'max_content_chars' => 12000,
                ],
                'privacy' => [
                    'no_drafts' => true,
                    'redact_pii' => true,
                ],
                // Optional on-site credits enforcement for external AI usage.
                // Credits are an internal budget unit (tokens-based by default).
                // Balance is stored separately (option: wpsn_ai_credits) and is not exported.
                'credits' => [
                    'enabled' => false,
                    // tokens | usd
                    'mode' => 'tokens',
                    // tokens-mode: 1 credit equals N tokens.
                    'tokens_per_credit' => 1000,
                    // usd-mode: credits per 1 USD.
                    'credits_per_usd' => 100.0,
                ],
                'external' => [
                    'enabled' => false,
                    'provider' => 'openai',
                    'model' => 'gpt-4.1-mini',
                    'max_output_tokens' => 180,
                    'brief_max_output_tokens' => 900,
                    'temperature' => 0.7,
                    'daily_token_limit' => 0,
                    'daily_request_limit' => 0,
                    'daily_budget_usd' => 0.0,
                    // Cost estimation (USD per 1M tokens). Keep 0 if unknown.
                    'price_input_per_1m' => 0.0,
                    'price_output_per_1m' => 0.0,
                    'store_responses' => false,
                    // Optional headers
                    'openai_org' => '',
                    'openai_project' => '',
                    // Server-side rate limiting for external AI REST endpoint (best-effort).
                    'ratelimit' => [
                        'enabled' => false,
                        'per_user' => true,
                        'requests_per_minute' => 30,
                    ],
                ],
                'prompts' => [
                    'title_template' => '',
                    'title_version'  => 1,
                    'description_template' => '',
                    'description_version'  => 1,
                    'alt_template' => '',
                    'alt_version'  => 1,
                    'brief_template' => '',
                    'brief_version'  => 1,
                    'serp_gap_template' => '',
                    'serp_gap_version'  => 1,
                    'serp_gap_faq_template' => '',
                    'serp_gap_faq_version'  => 1,
                ],
            ],

            // Trust & Privacy (local-only mode, external request log)
            'privacy' => [
                // Strict Trust Mode preset (runtime-only overrides).
                // When enabled, we force a set of privacy/security-focused behaviors
                // without mutating stored settings.
                'trust_mode' => false,
                // If enabled, external services are disabled at runtime (OpenAI, GSC sync, IndexNow, Webhooks).
                'local_only' => false,
                // UI-only: hide all AI-related UI.
                'hide_ai_ui' => false,
                // Trust Mode helper: disable 3rd-party install helper buttons (e.g. Site Kit).
                'disable_sitekit_install_helper' => false,
                // Trust Mode helper: disable setup/recommendation notices.
                'no_nags' => false,
                // Best-effort audit log of marked outbound HTTP requests.
                'log_external_requests' => true,
                // Days to keep the external request log.
                'retention_days' => 30,
            ],

            // Notifications center (trust events; email/Slack/webhook)
            'notifications' => [
                'enabled' => true,
                // info | warn | critical
                'min_severity' => 'warn',
                // Used by some sources to deduplicate repeated errors.
                'default_mute_days' => 7,
                // Optional: unify legacy notification sources into Notification Center.
                // off | inbox | route
                'integrations' => [
                    'drift_monitor' => [
                        'mode' => 'off',
                    ],
                    'gsc_digest' => [
                        'mode' => 'off',
                    ],
                ],
                'channels' => [
                    'email_enabled' => true,
                    // Comma/space separated list. If empty, admin_email is used.
                    'email_recipients' => '',
                    'slack_enabled' => false,
                    'slack_webhook' => '',
                    'webhook_enabled' => false,
                    // One URL per line.
                    'webhook_urls' => '',
                ],
                'sources' => [
                    'head_diff' => true,
                    'queue_failed' => true,
                    'audit_critical' => true,
                    'quality_gate' => false,
                    'saas_sync_failed' => true,
                    'serp_gap_failed' => false,
                    'platform_ops' => false,
                ],
                // Routing rules: per-source thresholds + dedupe windows + send budget.
                'rules' => [
                    // 0 = unlimited. Applies to all channels combined (best-effort).
                    'send_budget_per_hour' => 30,
                    // Per-source overrides (source = DB `source` field).
                    'sources' => [
                        'head_diff' => [
                            'min_severity' => 'warn',
                            'dedupe_window_minutes' => 0,
                        ],
                        'queue' => [
                            'min_severity' => 'warn',
                            'dedupe_window_minutes' => 0,
                        ],
                        'audit' => [
                            'min_severity' => 'warn',
                            'dedupe_window_minutes' => 0,
                        ],
                        'quality_gate' => [
                            'min_severity' => 'critical',
                            'dedupe_window_minutes' => 0,
                        ],
                        'saas_sync' => [
                            'min_severity' => 'warn',
                            'dedupe_window_minutes' => 1440,
                        ],
                        'serp_gap' => [
                            'min_severity' => 'warn',
                            'dedupe_window_minutes' => 0,
                        ],
                        'platform_ops' => [
                            'min_severity' => 'warn',
                            'dedupe_window_minutes' => 0,
                        ],
                        'drift_monitor' => [
                            'min_severity' => 'warn',
                            'dedupe_window_minutes' => 0,
                        ],
                        'search_console' => [
                            'min_severity' => 'warn',
                            'dedupe_window_minutes' => 0,
                        ],
                        'test' => [
                            'min_severity' => 'info',
                            'dedupe_window_minutes' => 0,
                        ],
                    ],
                ],
                // Optional digests (daily / weekly) generated by Notification Center.
                'digests' => [
                    'daily_enabled' => false,
                    // UTC hour (0..23).
                    'daily_hour_utc' => 9,
                    'weekly_enabled' => false,
                    // UTC weekday (1=Mon .. 7=Sun).
                    'weekly_weekday_utc' => 1,
                    // UTC hour (0..23).
                    'weekly_hour_utc' => 9,
                    // Max items in digest message.
                    'max_items' => 25,
                    // Minimum severity to include into digest.
                    'min_severity' => 'warn',
                    // If disabled, skip sending digests when there are 0 open items.
                    'send_when_empty' => false,
                ],
            ],


            // Activity log (admin actions)
            'activity_log' => [
                // Days to keep the activity log (0 = keep forever, purge disabled).
                'retention_days' => 90,
            ],

            // Audit (rule-based) scan history
            'audit' => [
                // Keep last N audit scans in DB (0 = keep forever, auto-purge disabled).
                'history_keep_scans' => 20,

                // Optional scheduled scans (WP-Cron hourly tick).
                'automation' => [
                    // off|daily|weekly
                    'schedule' => 'off',
                    // UTC hour (0..23)
                    'hour_utc' => 4,
                    // UTC weekday (0=Sun .. 6=Sat). Used only for Weekly.
                    'weekday_utc' => 1,
                    // Mirror audit scan toggles.
                    'include_terms' => true,
                    'check_social_image' => true,
                ],
            ],

            // Stage (roadmap): AI Search Readiness
            'ai_readiness' => [
                'llms' => [
                    'enabled' => true,
                    // auto | manual
                    'mode' => 'direct',
                    // manual markdown
                    'manual' => '',
                    'auto' => [
                        'include_pages' => true,
                        'include_posts' => false,
                        'max_links' => 20,
                    ],
                ],
                'bots' => [
                    'enabled' => true,
                    // map: bot_key => bool (allow)
                    'allow' => [
                        'oai_searchbot' => true,
                        'gptbot' => true,
                        'chatgpt_user' => true,
                        'claude_searchbot' => true,
                        'claudebot' => true,
                        'claude_user' => true,
                        'google_extended' => true,
                    ],
                    // Optional server-side blocker (Bot Blocker): blocks requests by User-Agent token.
                    'blocker' => [
                        'enabled' => false,
                        // 403 (hard) or 429 (soft with Retry-After)
                        'status_code' => 403,
                        'retry_after' => 60,
                        // Don't block logged-in users (safe default).
                        'allow_logged_in' => true,
                    ],
                ],
            ],
            'automation' => [
                'rest' => [
                    'enabled' => true,
                    'public_head' => [
                        'enabled' => false,
                        // seconds (0 = no cache)
                        'cache_ttl' => 60,
                    ],
                ],
                'webhooks' => [
                    // URLs are optional.
                    'meta_updated' => '',
                    'brief_generated' => '',
                    'redirect_created' => '',
                    'redirect_updated' => '',
                    'audit_scan_completed' => '',
                    'schema_scan_completed' => '',
                'schema_fix_completed' => '',
                    'schema_validation_failed' => '',
                    // Optional secret to sign payloads.
                    'secret' => '',
                    // seconds
                    'timeout' => 5,
                ],
            ],

            // Stage (roadmap): GSC Data Hub → Content gap → Internal linking suggestions
            'search_console' => [
                // selected property (siteUrl / sc-domain:...)
                'property' => '',
                // default fetch range
                'range_days' => 28,
                // Search Analytics rowLimit (max 25000)
                'row_limit' => 25000,
                // optional daily cron sync (module must be enabled)
                'scheduled_sync' => false,
                'content_gap' => [
                    'min_impressions' => 50,
                    // opportunities filter (for future)
                    'pos_min' => 8,
                    'pos_max' => 20,
                    'ctr_max' => 0.02,
                ],
                'internal_links' => [
                    'enabled' => true,
                    'only_orphans' => true,
                    'min_impressions' => 50,
                    'max_targets' => 30,
                    'max_queries_per_target' => 3,
                    'max_sources_per_query' => 10,
                    'max_suggestions_per_target' => 5,
                ],

                // Rank tracking (Stage G2): daily snapshots (date+page) and simple alerts.
                'rank_tracking' => [
                    'enabled' => true,
                    // Optional daily cron sync for snapshots.
                    'scheduled_sync' => false,
                    // How many days to refetch on each scheduled run (GSC data is delayed).
                    'backfill_days' => 7,
                    // Keep daily data for this many days.
                    'retention_days' => 180,

                    // Optional query-level tracking (day + query + page). Off by default (can be heavy).
                    'query_level' => [
                        'enabled' => false,
                        // GSC rowLimit per day.
                        'row_limit' => 5000,
                        // Query-level alerts: ignore rows with fewer impressions.
                        'min_impressions' => 50,
                        // Query-level alerts: max rows to scan per day.
                        'alert_rows_limit' => 500,
                    ],
                    'alerts' => [
                        'enabled' => true,
                        // Compare current day vs N days ago.
                        'compare_days' => 7,
                        // Filter noise.
                        'min_impressions' => 100,
                        // Trigger when position got worse by >= this amount.
                        'position_drop' => 3.0,
                        // Trigger when CTR dropped by >= this absolute amount (0..1).
                        'ctr_drop' => 0.02,

                        // Optional digest (email/slack).
                        'digest' => [
                            'enabled' => false,
                            'email_enabled' => true,
                            // comma-separated list; empty = admin_email
                            'email_recipients' => '',
                            'slack_enabled' => false,
                            'slack_webhook' => '',
                            'max_items' => 25,
                            'include_info' => false,
                            'include_warning' => true,
                            'include_critical' => true,

                            // Optional weekly digest (cron). Uses the same channels/settings as the daily digest.
                            'weekly_enabled' => false,
                            // 1 (Mon) .. 7 (Sun), UTC
                            'weekly_weekday' => 1,
                            'weekly_hour_utc' => 3,
                            'weekly_minute_utc' => 15,
                            // Number of days to include (usually 7).
                            'weekly_days' => 7,
                        ],
                    ],
                ],
            ],

            // IndexNow (instant indexing ping)
            'indexnow' => [
                'enabled' => false,
                'auto_submit' => true,
                'submit_terms' => true,
                'submit_redirects' => true,
                'endpoint' => 'https://api.indexnow.org/indexnow',
                // Max 10k by protocol; keep lower by default.
                'batch_size' => 100,
                // seconds: dedupe window for repeated events
                'dedupe_window' => 300,
                // seconds: backoff base and max on 429/503
                'base_backoff' => 30,
                'max_backoff' => 600,
                'sitemap_pings' => [
                    'enabled' => false,
                    'include_index' => false,
                    'include_news' => true,
                    'include_video' => true,
                    'providers' => [
                        'bing' => true,
                        'google' => false,
                    ],
                ],
            ],

            // Config Sync / Agency (lock settings UI to avoid accidental edits)
            
            'white_label' => [
                'enabled' => false,
                'brand_name' => 'WP SEO Next',
                'menu_title' => '',
                'settings_heading' => '',
                'hide_branding' => false,
                // Agency mode: hide upgrade/licensing UI (useful for client sites).
                'hide_upgrade_ui' => false,
            ],

            // Integrations (page builders, custom fields providers).
            'integrations' => [
                'elementor' => [
                    'enabled' => true,
                    'editor_controls' => true,
                ],
                'divi' => [
                    'enabled' => true,
                    'universal_metabox' => true,
                ],
                'beaver' => [
                    'enabled' => true,
                    'universal_metabox' => true,
                ],
                'custom_fields' => [
                    'enabled' => true,
                    'include_in_analysis' => false,
                    'analysis_keys' => '',
                    'max_value_chars' => 500,
                ],
            ],

'config_sync' => [
                'lock_settings' => false,
            ],

            'compatibility' => [
                'safe_mode' => false,
                'disable_output' => false,
                // If enabled and another SEO plugin is detected, WP SEO Next will disable its output modules
                // (Meta/Social/Schema/Sitemap/Robots/Breadcrumbs) at runtime to prevent duplicate tags.
                // This is a runtime safeguard; it does not mutate stored module settings.
                'auto_disable_output_on_conflict' => false,
            ],



            // SEO Drift Monitor (local head drift + unified view).
            'drift_monitor' => [
                'enabled' => true,
                // How many top GSC pages to try to auto-add.
                'seed_gsc_limit' => 20,
                'gsc' => [
                    // Enable CTR/position drift rules inside Drift Monitor.
                    'enabled' => true,
                    // Daily ensure top pages are added as URL-items.
                    'auto_track_top_pages' => false,
                    // Window length (in full days) for metrics aggregation.
                    'window_days' => 7,
                    // Avoid noisy low-volume pages.
                    'min_impressions' => 100,
                    // Absolute CTR drop thresholds (percentage points).
                    'ctr_drop_warn_pct' => 2.0,
                    'ctr_drop_critical_pct' => 5.0,
                    // Position worsening thresholds (+ positions).
                    'position_drop_warn' => 2.0,
                    'position_drop_critical' => 5.0,
                ],
                'notifications' => [
                    'enabled' => false,
                    // info|warn|critical
                    'min_severity' => 'warn',
                    // How long to mute an item after acknowledging an event.
                    'mute_days' => 7,
                    'email_enabled' => false,
                    'email_recipients' => '',
                    'slack_enabled' => false,
                    'slack_webhook' => '',
                    'webhook_enabled' => false,
                    // One URL per line.
                    'webhook_urls' => '',
                ],
            ],
            'modules'   => [],

            // Performance budget (lightweight controls for background processing).
            'performance' => [
                'queue' => [
                    // auto = WP-Cron scheduled, direct = run inline after enqueue (no schedule), manual = no schedule (run via WP-CLI / tools).
                    'mode' => 'direct',
                    // Allowed: 1, 2, 5, 10, 15
                    'interval_minutes' => 1,
                    // Max jobs per tick.
                    'max_jobs' => 5,

                    // Direct mode worker limits (applies when Queue mode is Direct).
                    'direct' => [
                        // Max jobs to process at the end of a request.
                        'max_jobs' => 200,
                        // Max seconds to spend processing jobs per request.
                        'max_seconds' => 20,
                        // If a job is scheduled within this many seconds, clamp it to "now" to allow job chains to complete.
                        'eager_run_at_seconds' => 5,
                    ],

                    // Auto-pause the queue when it repeatedly sees transient overload responses.
                    // This is particularly useful on shared hosting to avoid "retry storms".
                    'autopause' => [
                        'enabled' => true,
                        // Number of 503/429 signals within the window to trigger pause.
                        'threshold' => 3,
                        // Minutes.
                        'window_minutes' => 10,
                        // Minutes.
                        'pause_minutes' => 30,
                    ],
                ],
                'admin' => [
                    // If enabled, disables editor/page builder integrations to keep wp-admin fast.
                    'minimize_admin_footprint' => false,
                    // If enabled, collect lightweight request metrics for diagnostics.
                    'collect_metrics' => false,
                    // If enabled, store a rolling history of perf snapshots.
                    'record_history' => false,
                    // Optional live-mode UI using admin-ajax.php (disabled by default for stability on shared hosting).
                    'ajax_enabled' => false,
                    // If enabled, WP SEO Next avoids fully booting on unrelated admin-ajax.php requests.
                    'ajax_lite_boot' => true,
                ],

                'frontend' => [
                    // Enforce a strict frontend asset contract (no WPSN JS/CSS for visitors).
                    'contract_enabled' => true,
                    // Load the Frontend Inspector only when explicitly requested (admin bar shortcut adds ?wpsn_inspector=1).
                    'inspector_on_demand' => true,
                ],
            ],
        ];
    }
}
