HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: /home/xedaptot/hi.naniguide.com/app/Http/Controllers/Admin/SettingController.php
<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\SettingUpdateRequest;
use App\Models\DeliveryServer;
use App\Models\CustomerGroup;
use App\Models\ExternalTemplate;
use App\Models\Plan;
use App\Models\Setting;
use App\Services\DeliveryServerService;
use App\Services\SettingService;
use App\Services\UpdateServerService;
use App\Translation\LocaleJsonService;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Str;
use ZipArchive;

class SettingController extends Controller
{
    public function __construct(
        protected SettingService $settingService,
        protected UpdateServerService $updateServerService
    ) {}

    private function billingKeysToHide(): array
    {
        return [
            'billing_provider',
            'billing_providers',

            'stripe_public_key',
            'stripe_secret',
            'stripe_webhook_secret',

            'paypal_client_id',
            'paypal_client_secret',

            'uddoktapay_api_key',
            'uddoktapay_base_url',

            'razorpay_key_id',
            'razorpay_key_secret',
            'razorpay_webhook_secret',

            'paystack_public_key',
            'paystack_secret',

            'flutterwave_public_key',
            'flutterwave_secret',
            'flutterwave_encryption_key',
            'flutterwave_webhook_secret',

            'manual_bank_name',
            'manual_account_name',
            'manual_account_number',
            'manual_instructions',
            'manual_qr_image_path',
        ];
    }

    private function normalizePurchaseCode(?string $code): string
    {
        $code = is_string($code) ? $code : '';
        $code = trim($code);

        $code = str_replace([
            "\u{2010}",
            "\u{2011}",
            "\u{2012}",
            "\u{2013}",
            "\u{2014}",
            "\u{2015}",
            "\u{2212}",
            "\u{FE63}",
            "\u{FF0D}",
        ], '-', $code);

        $code = preg_replace('/[\s\x{00A0}\x{200B}\x{200C}\x{200D}\x{FEFF}]+/u', '', $code) ?? '';

        if ($code !== '' && preg_match('/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i', $code, $m)) {
            return $m[0];
        }

        $code = preg_replace('/[^0-9a-f\-]/i', '', $code) ?? '';

        if ($code !== '' && preg_match('/^[0-9a-f]{32}$/i', $code)) {
            return substr($code, 0, 8)
                . '-' . substr($code, 8, 4)
                . '-' . substr($code, 12, 4)
                . '-' . substr($code, 16, 4)
                . '-' . substr($code, 20, 12);
        }

        if ($code !== '' && preg_match('/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i', $code, $m2)) {
            return $m2[0];
        }

        return $code;
    }

    private function templateDefinitions(): array
    {
        return [
            'home-5' => [
                'id' => 'home-5',
                'name' => 'Modern Landing',
                'variant' => '5',
                'is_pro' => false,
            ],
            'home-1-pro' => [
                'id' => 'home-1-pro',
                'name' => 'Classic SaaS',
                'variant' => '1',
                'is_pro' => true,
            ],
            'home-2-pro' => [
                'id' => 'home-2-pro',
                'name' => 'Minimal Startup',
                'variant' => '2',
                'is_pro' => true,
            ],
        ];
    }

    private function getTemplateOrAbort(string $templateId): array
    {
        $templateId = trim($templateId);
        $templates = $this->templateDefinitions();
        if (!array_key_exists($templateId, $templates)) {
            abort(404);
        }

        $template = $templates[$templateId];
        if (!is_array($template)) {
            abort(404);
        }

        return $template;
    }

    private function ensureTemplateUnlocked(array $template): void
    {
        $isPro = (bool) ($template['is_pro'] ?? false);
        if ($isPro) {
            abort(403);
        }
    }

    private function templateEditableKeys(string $templateId): array
    {
        if ($templateId === 'home-5') {
            return [
                'brand_color',
                'hero_title',
                'hero_subtitle',
                'cta_text',
                'cta_secondary_text',
                'stat_emails_sent',
                'stat_users',
                'stat_uptime',
                'integration_1_icon',
                'integration_2_icon',
                'integration_3_icon',
                'integration_4_icon',
                'integration_5_icon',
                'integration_6_icon',
            ];
        }

        return ['brand_color'];
    }

    private function templateValidationRules(string $templateId): array
    {
        $rules = [];
        foreach ($this->templateEditableKeys($templateId) as $key) {
            $rules[$key] = ['nullable', 'string', 'max:500'];
        }

        if (array_key_exists('brand_color', $rules)) {
            $rules['brand_color'] = ['nullable', 'string', 'regex:/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/'];
        }

        foreach (['integration_1_icon', 'integration_2_icon', 'integration_3_icon', 'integration_4_icon', 'integration_5_icon', 'integration_6_icon'] as $k) {
            if (array_key_exists($k, $rules)) {
                $rules[$k] = ['nullable', 'string', 'max:60', 'regex:/^[a-z0-9\-]+$/'];
            }
        }

        return $rules;
    }

    private function templateDefaultValues(string $templateId): array
    {
        if ($templateId === 'home-5') {
            return [
                'brand_color' => '#3b82f6',
                'hero_title' => 'The Future of Email Marketing',
                'hero_subtitle' => 'Launch powerful email campaigns with our self-hosted platform. Complete control, no limits, maximum deliverability.',
                'cta_text' => 'Start Free Trial',
                'cta_secondary_text' => 'Watch Demo',
                'stat_emails_sent' => '50M+',
                'stat_users' => '10K+',
                'stat_uptime' => '99.9%',
                'integration_1_icon' => 'zap',
                'integration_2_icon' => 'message-square',
                'integration_3_icon' => 'globe',
                'integration_4_icon' => 'shopping-bag',
                'integration_5_icon' => 'credit-card',
                'integration_6_icon' => 'target',
            ];
        }

        return [
            'brand_color' => '#3b82f6',
        ];
    }

    private function templateSettingKey(string $templateId, string $key): string
    {
        return 'template_' . $templateId . '_' . $key;
    }

    private function loadTemplateValues(string $templateId, array $overrides = []): array
    {
        $defaults = $this->templateDefaultValues($templateId);
        $values = [];

        foreach ($this->templateEditableKeys($templateId) as $key) {
            $settingKey = $this->templateSettingKey($templateId, $key);
            $fallback = $defaults[$key] ?? null;

            try {
                $stored = Setting::get($settingKey, $fallback);
            } catch (\Throwable $e) {
                $stored = $fallback;
            }

            $values[$key] = is_string($stored) ? (string) $stored : (is_null($stored) ? null : (string) $stored);
        }

        foreach ($overrides as $k => $v) {
            if (!in_array($k, $this->templateEditableKeys($templateId), true)) {
                continue;
            }
            $values[$k] = is_string($v) ? $v : (is_null($v) ? null : (string) $v);
        }

        if (array_key_exists('brand_color', $values)) {
            $bc = $values['brand_color'];
            if (!is_string($bc) || trim($bc) === '' || !preg_match('/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/', trim($bc))) {
                $values['brand_color'] = $defaults['brand_color'] ?? '#3b82f6';
            }
        }

        return $values;
    }

    /**
     * Display the settings page.
     */
    public function index(Request $request)
    {
        Setting::firstOrCreate(
            ['key' => 'site_language'],
            [
                'category' => 'general',
                'value' => 'en',
                'type' => 'string',
                'description' => 'Site language used as default locale.',
                'is_public' => false,
            ]
        );

        Setting::firstOrCreate(
            ['key' => 'brand_color'],
            [
                'category' => 'appearance',
                'value' => '#3b82f6',
                'type' => 'string',
                'description' => 'Primary brand color used across the public website (buttons, links, borders).',
                'is_public' => true,
            ]
        );

        Setting::firstOrCreate(
            ['key' => 'external_templates_api_base_url'],
            [
                'category' => 'templates',
                'value' => '',
                'type' => 'string',
                'description' => 'Base URL for the external templates API (e.g. https://example.com).',
                'is_public' => false,
            ]
        );
        Setting::firstOrCreate(
            ['key' => 'external_templates_api_product_id'],
            [
                'category' => 'templates',
                'value' => '',
                'type' => 'string',
                'description' => 'External templates product_id used when syncing templates.',
                'is_public' => false,
            ]
        );
        Setting::firstOrCreate(
            ['key' => 'external_templates_api_resource_type'],
            [
                'category' => 'templates',
                'value' => 'Post',
                'type' => 'string',
                'description' => 'External templates resource_type used when syncing templates (e.g. Post).',
                'is_public' => false,
            ]
        );
        Setting::firstOrCreate(
            ['key' => 'external_templates_license_key'],
            [
                'category' => 'templates',
                'value' => '',
                'type' => 'string',
                'description' => 'License key for unlocking Pro external templates.',
                'is_public' => false,
            ]
        );
        Setting::firstOrCreate(
            ['key' => 'external_templates_license_active'],
            [
                'category' => 'templates',
                'value' => 0,
                'type' => 'boolean',
                'description' => 'Whether the external templates license is active.',
                'is_public' => false,
            ]
        );

        Setting::firstOrCreate(
            ['key' => 'toast_position'],
            [
                'category' => 'general',
                'value' => 'bottom_center',
                'type' => 'string',
                'description' => 'Where toast notifications appear across the dashboard (top/bottom + left/center/right).',
                'is_public' => false,
            ]
        );

        Setting::firstOrCreate(
            ['key' => 'public_meta_image'],
            [
                'category' => 'appearance',
                'value' => null,
                'type' => 'string',
                'description' => 'Default meta/OG image used for social sharing (Open Graph / Twitter) on the public website.',
                'is_public' => true,
            ]
        );

        Setting::firstOrCreate(
            ['key' => 'cron_web_enabled'],
            [
                'category' => 'cron',
                'value' => 0,
                'type' => 'boolean',
                'description' => 'Enable/disable running the Laravel scheduler via the Web Cron URL.',
                'is_public' => false,
            ]
        );

        Setting::firstOrCreate(
            ['key' => 'cron_web_token'],
            [
                'category' => 'cron',
                'value' => Str::random(40),
                'type' => 'string',
                'description' => 'Security token for the Web Cron URL. Regenerate if it leaks.',
                'is_public' => false,
            ]
        );

        Setting::firstOrCreate(
            ['key' => 'cron_last_run_at'],
            [
                'category' => 'cron',
                'value' => null,
                'type' => 'string',
                'description' => 'Last time the scheduler was triggered by Web Cron or Run Now.',
                'is_public' => false,
            ]
        );

        Setting::firstOrCreate(
            ['key' => 'home_page_variant'],
            [
                'category' => 'general',
                'value' => '1',
                'type' => 'string',
                'description' => 'Select which homepage design to use for the public website.',
                'is_public' => true,
            ]
        );

        Setting::firstOrCreate(
            ['key' => 'home_redirect_enabled'],
            [
                'category' => 'general',
                'value' => 0,
                'type' => 'boolean',
                'description' => 'When enabled, visitors to the home page (/) will be redirected to the URL below.',
                'is_public' => true,
            ]
        );

        Setting::firstOrCreate(
            ['key' => 'home_redirect_url'],
            [
                'category' => 'general',
                'value' => '',
                'type' => 'string',
                'description' => 'Redirect destination. Use a full URL (https://...) or a path starting with /.',
                'is_public' => true,
            ]
        );

        foreach (['1', '2', '3', '4'] as $homeVariant) {
            Setting::firstOrCreate(
                ['key' => 'home_' . $homeVariant . '_text_map'],
                [
                    'category' => 'homepage',
                    'value' => json_encode([]),
                    'type' => 'json',
                    'description' => 'JSON map of {"original text": "replacement"} for Home ' . $homeVariant . '. Exact match required.',
                    'is_public' => true,
                ]
            );
        }

        $navDefaults = [
            'nav_show_features' => 1,
            'nav_show_pricing' => 1,
            'nav_show_blog' => 1,
            'nav_show_roadmap' => 1,
            'nav_show_docs' => 1,
            'nav_show_api_docs' => 1,
        ];

        foreach ($navDefaults as $key => $defaultValue) {
            Setting::firstOrCreate(
                ['key' => $key],
                [
                    'category' => 'navigation',
                    'value' => $defaultValue,
                    'type' => 'boolean',
                    'description' => 'Show/hide this item in the public navbar.',
                    'is_public' => true,
                ]
            );
        }

        Setting::firstOrCreate(
            ['key' => 'blog_enabled'],
            [
                'category' => 'navigation',
                'value' => 1,
                'type' => 'boolean',
                'description' => 'Enable or disable the public blog completely.',
                'is_public' => false,
            ]
        );

        Setting::firstOrCreate(
            ['key' => 'docs_enabled'],
            [
                'category' => 'navigation',
                'value' => 1,
                'type' => 'boolean',
                'description' => 'Enable or disable the documentation page. When enabled, it is available to admins only.',
                'is_public' => false,
            ]
        );

        Setting::firstOrCreate(
            ['key' => 'google_analytics_tracking_id'],
            [
                'category' => 'general',
                'value' => null,
                'type' => 'string',
                'description' => 'Google Tag Manager Container ID (e.g. GTM-XXXXXXX).',
                'is_public' => true,
            ]
        );

        Setting::firstOrCreate(
            ['key' => 'meta_pixel_id'],
            [
                'category' => 'general',
                'value' => null,
                'type' => 'string',
                'description' => 'Meta Pixel ID (numbers only) for Facebook/Instagram conversion tracking.',
                'is_public' => true,
            ]
        );

        Setting::firstOrCreate(
            ['key' => 'home_page_title'],
            [
                'category' => 'general',
                'value' => 'Self-Hosted Email Marketing Platform',
                'type' => 'string',
                'description' => 'Browser title used on public home page variants.',
                'is_public' => true,
            ]
        );

        Setting::firstOrCreate(
            ['key' => 'pricing_section_badge'],
            [
                'category' => 'pricing',
                'value' => 'Our Pricing',
                'type' => 'string',
                'description' => 'Badge text shown above the pricing section title.',
                'is_public' => true,
            ]
        );
        Setting::firstOrCreate(
            ['key' => 'pricing_section_title'],
            [
                'category' => 'pricing',
                'value' => 'Simple, transparent pricing',
                'type' => 'string',
                'description' => 'Main heading of the public pricing section.',
                'is_public' => true,
            ]
        );
        Setting::firstOrCreate(
            ['key' => 'pricing_section_subtitle'],
            [
                'category' => 'pricing',
                'value' => "Choose the plan that's right for your business. All plans include a 14-day free trial.",
                'type' => 'string',
                'description' => 'Subtitle shown under the pricing section heading.',
                'is_public' => true,
            ]
        );
        Setting::firstOrCreate(
            ['key' => 'pricing_section_toggle_monthly'],
            [
                'category' => 'pricing',
                'value' => 'Pay Monthly',
                'type' => 'string',
                'description' => 'Label for the monthly billing toggle.',
                'is_public' => true,
            ]
        );
        Setting::firstOrCreate(
            ['key' => 'pricing_section_toggle_annual'],
            [
                'category' => 'pricing',
                'value' => 'Pay Annually',
                'type' => 'string',
                'description' => 'Label for the annual billing toggle.',
                'is_public' => true,
            ]
        );
        Setting::firstOrCreate(
            ['key' => 'pricing_section_toggle_save'],
            [
                'category' => 'pricing',
                'value' => '(save 20%)',
                'type' => 'string',
                'description' => 'Savings label shown next to the annual toggle.',
                'is_public' => true,
            ]
        );
        Setting::firstOrCreate(
            ['key' => 'pricing_section_show_all'],
            [
                'category' => 'pricing',
                'value' => 0,
                'type' => 'boolean',
                'description' => 'Show all plans together instead of toggling monthly/annual.',
                'is_public' => true,
            ]
        );
        Setting::firstOrCreate(
            ['key' => 'pricing_section_columns'],
            [
                'category' => 'pricing',
                'value' => 3,
                'type' => 'integer',
                'description' => 'Number of pricing plan columns displayed (1-5).',
                'is_public' => true,
            ]
        );
        Setting::firstOrCreate(
            ['key' => 'pricing_section_popular_badge'],
            [
                'category' => 'pricing',
                'value' => 'Popular',
                'type' => 'string',
                'description' => 'Badge text displayed on the most popular plan.',
                'is_public' => true,
            ]
        );

        Setting::firstOrCreate(
            ['key' => 'transactional_delivery_server_id'],
            [
                'category' => 'email',
                'value' => null,
                'type' => 'string',
                'description' => 'Default delivery server for system transactional emails (verification/password reset). Use "system" to use MAIL_* from .env.',
                'is_public' => false,
            ]
        );

        Setting::firstOrCreate(
            ['key' => 'verification_delivery_server_id'],
            [
                'category' => 'email',
                'value' => null,
                'type' => 'string',
                'description' => 'Delivery server for verification emails. Use "inherit" to use the transactional default, or "system" to use MAIL_* from .env.',
                'is_public' => false,
            ]
        );

        Setting::firstOrCreate(
            ['key' => 'password_reset_delivery_server_id'],
            [
                'category' => 'email',
                'value' => null,
                'type' => 'string',
                'description' => 'Delivery server for password reset emails. Use "inherit" to use the transactional default, or "system" to use MAIL_* from .env.',
                'is_public' => false,
            ]
        );

        Setting::firstOrCreate(
            ['key' => 'billing_default_provider'],
            [
                'category' => 'billing',
                'value' => 'stripe',
                'type' => 'string',
                'description' => 'Default payment provider for new subscriptions.',
                'is_public' => false,
            ]
        );

        Setting::firstOrCreate(
            ['key' => 'gdpr_notice_title'],
            [
                'category' => 'privacy',
                'value' => 'We value your privacy',
                'type' => 'string',
                'description' => 'GDPR notice title shown on the public website.',
                'is_public' => true,
            ]
        );

        Setting::firstOrCreate(
            ['key' => 'gdpr_notice_description'],
            [
                'category' => 'privacy',
                'value' => 'We use cookies and similar technologies to improve your experience. You can accept or decline.',
                'type' => 'string',
                'description' => 'GDPR notice description shown on the public website.',
                'is_public' => true,
            ]
        );

        Setting::firstOrCreate(
            ['key' => 'gdpr_notice_accept_text'],
            [
                'category' => 'privacy',
                'value' => 'Accept',
                'type' => 'string',
                'description' => 'Text for the accept button.',
                'is_public' => true,
            ]
        );

        Setting::firstOrCreate(
            ['key' => 'gdpr_notice_decline_text'],
            [
                'category' => 'privacy',
                'value' => 'Decline',
                'type' => 'string',
                'description' => 'Text for the decline button.',
                'is_public' => true,
            ]
        );

        Setting::firstOrCreate(
            ['key' => 'gdpr_notice_position'],
            [
                'category' => 'privacy',
                'value' => 'bottom_full_width',
                'type' => 'string',
                'description' => 'Banner position on the public website.',
                'is_public' => true,
            ]
        );

        Setting::firstOrCreate(
            ['key' => 'gdpr_notice_delay_seconds'],
            [
                'category' => 'privacy',
                'value' => 0,
                'type' => 'integer',
                'description' => 'Delay (in seconds) before showing the GDPR notice.',
                'is_public' => true,
            ]
        );

        $category = $request->get('category', 'general');
        $settings = $this->settingService->getSettingsByCategory();
        $categories = $this->settingService->getCategories();

        $categories = array_values(array_filter($categories, fn ($cat) => !in_array($cat, ['homepage', 'templates', 'addon', 'addons', 'site_pages', 'unibox'], true)));

        if ($settings->has('billing')) {
            $keysToHide = $this->billingKeysToHide();
            $settings['billing'] = $settings['billing']->reject(fn ($s) => in_array((string) ($s->key ?? ''), $keysToHide, true));
        }

        $seoKeys = ['home_page_title', 'meta_description', 'meta_keywords', 'meta_pixel_id', 'google_analytics_tracking_id', 'public_meta_image', 'site_favicon'];
        $settings['seo'] = Setting::whereIn('key', $seoKeys)->orderBy('key')->get();
        if ($settings->has('general')) {
            $settings['general'] = $settings['general']->reject(fn ($s) => in_array((string) ($s->key ?? ''), $seoKeys, true));
        }
        if ($settings->has('appearance')) {
            $settings['appearance'] = $settings['appearance']->reject(fn ($s) => in_array((string) ($s->key ?? ''), $seoKeys, true));
        }

        $generalSettingsKeys = ['home_page_variant', 'home_redirect_enabled', 'home_redirect_url', 'toast_position'];
        if ($settings->has('appearance')) {
            $movedGeneralSettings = $settings['appearance']->filter(fn ($s) => in_array((string) ($s->key ?? ''), $generalSettingsKeys, true));

            if ($movedGeneralSettings->isNotEmpty()) {
                $settings['appearance'] = $settings['appearance']->reject(fn ($s) => in_array((string) ($s->key ?? ''), $generalSettingsKeys, true));
                $settings['general'] = $settings->has('general')
                    ? $settings['general']->merge($movedGeneralSettings)->sortBy('key')->values()
                    : $movedGeneralSettings->sortBy('key')->values();
            }
        }

        if (!in_array($category, $categories, true)) {
            $category = $categories[0] ?? 'general';
        }

        $customerGroups = [];
        $plans = [];
        if ($category === 'account') {
            $customerGroups = CustomerGroup::orderBy('name')->pluck('name', 'id')->toArray();
            $plans = Plan::where('is_active', true)->orderBy('name')->pluck('name', 'id')->toArray();

            Setting::firstOrCreate(
                ['key' => 'new_registered_customer_plan_id'],
                [
                    'category' => 'account',
                    'value' => null,
                    'type' => 'integer',
                    'description' => 'Pricing plan automatically assigned to newly registered customers.',
                    'is_public' => false,
                ]
            );
        }

        $deliveryServerOptions = [];
        if ($category === 'email') {
            $deliveryServerOptions[''] = 'Auto';
     
             try {
                 $deliveryServerService = app(DeliveryServerService::class);
                 $systemSmtpServer = $deliveryServerService->getOrCreateSystemSmtpDeliveryServer();
                 if ($systemSmtpServer) {
                     $deliveryServerOptions[(string) $systemSmtpServer->id] = 'System (SMTP)';
                 }
             } catch (\Throwable $e) {
                 // ignore
             }
     
             $activeServers = app(DeliveryServerService::class)
                 ->queryAdminEmailSettingDeliveryServers()
                 ->get(['id', 'name', 'type']);
 
             foreach ($activeServers as $server) {
                 $label = $server->name;
                 if (is_string($server->type) && trim($server->type) !== '') {
                     $label .= ' (' . strtoupper(str_replace('-', ' ', $server->type)) . ')';
                }

                $deliveryServerOptions[(string) $server->id] = $label;
            }
        }

        $updateProduct = null;
        $updateChangelogs = null;
        $updateDownloadUrl = null;
        $updateConfig = null;
        $updateLicenseCheck = null;
        $updateStatus = null;
        $updateInstallState = null;
        $updateLastFailureAt = null;
        $updateLastFailureVersion = null;
        $updateLastFailureReason = null;

        $updateStatus = Cache::get('update_server:update_status');
        $updateInstallState = Setting::get('update_install_state');
        $updateLastFailureAt = Setting::get('update_last_failure_at');
        $updateLastFailureVersion = Setting::get('update_last_failure_version');
        $updateLastFailureReason = Setting::get('update_last_failure_reason');

        if (in_array($category, ['updates', 'changelogs'], true)) {
            $refresh = $request->boolean('refresh');
            $refreshUpdates = $category === 'updates' ? true : $refresh;
            $refreshChangelogs = $category === 'changelogs' ? true : $refresh;

            if ($category === 'updates' && $refreshUpdates) {
                try {
                    Artisan::call('updates:check', ['--force' => true]);
                } catch (\Throwable $e) {
                    // ignore
                }

                $updateStatus = Cache::get('update_server:update_status');
            }

            $baseUrl = (string) config('services.update_server.base_url', Setting::get('update_api_base_url'));
            $productId = config('services.update_server.product_id', Setting::get('update_product_id'));
            $productName = (string) config('services.update_server.product_name', Setting::get('update_product_name'));
            $productSecret = (string) config('services.update_server.product_secret', Setting::get('update_product_secret'));

            $appUrl = (string) config('app.url');
            $parsed = parse_url($appUrl);
            $domain = is_array($parsed) && is_string($parsed['host'] ?? null) && trim((string) $parsed['host']) !== ''
                ? (string) $parsed['host']
                : $appUrl;

            $licenseKey = Setting::get('update_license_key');
            $licenseKey = $this->normalizePurchaseCode(is_string($licenseKey) ? $licenseKey : null);

            if (is_string($licenseKey) && trim($licenseKey) !== '' && preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $licenseKey)) {
                $cacheKey = 'update_server:license_check:' . md5((string) $baseUrl) . ':' . md5((string) $licenseKey) . ':' . md5((string) $domain);
                if ($refresh) {
                    Cache::forget($cacheKey);
                }

                $updateLicenseCheck = Cache::remember($cacheKey, now()->addMinutes(10), function () use ($licenseKey, $domain, $productSecret, $productName) {
                    return $this->updateServerService->licenseCheck(
                        (string) $licenseKey,
                        $domain,
                        (string) $productSecret,
                        (string) $productName
                    );
                });
            }

            $updateConfig = [
                'base_url' => $baseUrl,
                'product_id' => $productId,
                'product_name' => $productName,
                'has_product_secret' => is_string($productSecret) && trim($productSecret) !== '',
            ];

            $productIdToUse = is_numeric($productId) ? (int) $productId : null;

            $baseUrlOk = is_string($baseUrl) && trim($baseUrl) !== '';
            $productIdOk = is_numeric($productIdToUse) && (int) $productIdToUse > 0;

            if ($baseUrlOk && $productIdOk) {
                if ($updateProduct === null) {
                    $updateProduct = $this->updateServerService->getProduct((int) $productIdToUse, (string) $baseUrl, $refreshUpdates);
                }

                if ($category !== 'changelogs' || ($updateChangelogs === null)) {
                    $updateChangelogs = $this->updateServerService->getChangelogs((int) $productIdToUse, (string) $baseUrl, $refreshChangelogs);
                }
            } else {
                $updateProduct = [
                    'success' => false,
                    'data' => null,
                    'message' => 'Update server settings are not configured yet.',
                    'status' => null,
                ];
                $updateChangelogs = $updateProduct;
            }

            $updateDownloadUrl = session()->pull('update_download_url');
        }

        $activeLocales = app(LocaleJsonService::class)->listActiveLocales();

        return view('admin.settings.index', compact(
            'settings',
            'categories',
            'category',
            'customerGroups',
            'plans',
            'deliveryServerOptions',
            'activeLocales',
            'updateProduct',
            'updateChangelogs',
            'updateDownloadUrl',
            'updateConfig',
            'updateLicenseCheck',
            'updateStatus',
            'updateInstallState',
            'updateLastFailureAt',
            'updateLastFailureVersion',
            'updateLastFailureReason'
        ));
    }

    public function logs(Request $request)
    {
        $perPage = (int) $request->query('per_page', 20);
        if (!in_array($perPage, [10, 20, 30, 50], true)) {
            $perPage = 20;
        }

        $page = max(1, (int) $request->query('page', 1));

        $logPath = storage_path('logs/laravel.log');
        if (!is_file($logPath) || !is_readable($logPath)) {
            $logs = new LengthAwarePaginator([], 0, $perPage, $page, [
                'path' => $request->url(),
                'query' => $request->query(),
            ]);

            return view('admin.settings.logs', [
                'logs' => $logs,
                'perPage' => $perPage,
                'logFile' => null,
                'logFileSize' => null,
            ])->with('error', 'Log file not found.');
        }

        $fileSize = @filesize($logPath);
        $fileSize = is_int($fileSize) ? $fileSize : null;

        $bytesToRead = 2 * 1024 * 1024;
        $entries = [];

        try {
            $fp = fopen($logPath, 'rb');
            if ($fp !== false) {
                $seek = 0;
                if (is_int($fileSize)) {
                    $seek = max(0, $fileSize - $bytesToRead);
                }

                if ($seek > 0) {
                    fseek($fp, $seek);
                    fgets($fp);
                }

                $chunk = stream_get_contents($fp);
                fclose($fp);

                $entries = $this->parseLogEntries(is_string($chunk) ? $chunk : '');
            }
        } catch (\Throwable $e) {
            $entries = [];
        }

        $total = count($entries);
        $items = array_slice($entries, ($page - 1) * $perPage, $perPage);

        $logs = new LengthAwarePaginator($items, $total, $perPage, $page, [
            'path' => $request->url(),
            'query' => $request->query(),
        ]);

        return view('admin.settings.logs', [
            'logs' => $logs,
            'perPage' => $perPage,
            'logFile' => $logPath,
            'logFileSize' => $fileSize,
        ]);
    }

    private function parseLogEntries(string $content): array
    {
        $content = str_replace("\r\n", "\n", $content);
        $lines = explode("\n", $content);

        $entries = [];
        $current = '';

        $isHeader = function (string $line): bool {
            return (bool) preg_match('/^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]/', $line);
        };

        foreach ($lines as $line) {
            if ($line === '') {
                continue;
            }

            if ($isHeader($line)) {
                if ($current !== '') {
                    $entries[] = $current;
                }
                $current = $line;
                continue;
            }

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

            $current .= "\n" . $line;
        }

        if ($current !== '') {
            $entries[] = $current;
        }

        $entries = array_reverse($entries);

        return array_values($entries);
    }

    public function regenerateCronToken(Request $request)
    {
        Setting::set('cron_web_token', Str::random(40), 'cron', 'string');

        return redirect()
            ->route('admin.settings.index', ['category' => 'cron'])
            ->with('success', 'Cron token regenerated successfully. Update your cPanel cron job to use the new URL.');
    }

    public function runCronNow(Request $request)
    {
        $exitCode = 1;
        $output = '';

        try {
            $exitCode = Artisan::call('schedule:run');
            $output = (string) Artisan::output();
        } catch (\Throwable $e) {
            $output = $e->getMessage();
        }

        Setting::set('cron_last_run_at', now()->toIso8601String(), 'cron', 'string');

        return redirect()
            ->route('admin.settings.index', ['category' => 'cron'])
            ->with('success', 'Scheduler run finished (exit code: ' . (int) $exitCode . ').')
            ->with('cron_run_output', $output);
    }

    public function installUpdate(Request $request)
    {
        $targetVersion = $request->input('target_version');
        $targetVersion = is_string($targetVersion) ? trim($targetVersion) : '';

        if ($targetVersion === '') {
            return redirect()
                ->route('admin.settings.index', ['category' => 'updates'])
                ->with('error', 'Latest version is not available to install yet. Please refresh and try again.');
        }

        $installedVersion = trim((string) config('mailpurse.version', ''));
        if ($installedVersion === '') {
            $installedVersion = Setting::get('app_version');
            $installedVersion = is_string($installedVersion) ? trim($installedVersion) : '';
        }
        if ($installedVersion !== '' && version_compare($targetVersion, $installedVersion, '<=')) {
            return redirect()
                ->route('admin.settings.index', ['category' => 'updates'])
                ->with('error', 'No update available.');
        }

        $state = Setting::get('update_install_state');
        if (
            is_array($state)
            && (
                (bool) ($state['in_progress'] ?? false)
                || in_array((string) ($state['status'] ?? ''), ['queued', 'running'], true)
            )
        ) {
            return redirect()
                ->route('admin.settings.index', ['category' => 'updates'])
                ->with('error', 'An update is already running. Please wait.');
        }

        // Set initial state for UI display
        Setting::set('update_install_state', [
            'in_progress' => true,
            'status' => 'running',
            'message' => 'Update started.',
            'version' => $targetVersion,
            'started_at' => now()->toIso8601String(),
        ], 'updates', 'json');

        // Dispatch update job immediately instead of queuing for cron
        \App\Jobs\InstallUpdateJob::dispatch($targetVersion);

        return redirect()
            ->route('admin.settings.index', ['category' => 'updates'])
            ->with('success', 'Update started. The site will enter maintenance mode during installation.');
    }

    public function cancelUpdate(): \Illuminate\Http\RedirectResponse
    {
        $state = Setting::get('update_install_state');
        if (!is_array($state) || !(bool) ($state['in_progress'] ?? false)) {
            return redirect()
                ->route('admin.settings.index', ['category' => 'updates'])
                ->with('info', 'No update is currently in progress.');
        }

        Setting::set('update_install_state', [
            'in_progress' => false,
            'status' => 'aborted',
            'message' => 'Update cancelled by user.',
            'finished_at' => now()->toIso8601String(),
        ], 'updates', 'json');

        try {
            Artisan::call('up');
        } catch (\Throwable $e) {
            Log::warning('Cancel update failed to bring site out of maintenance mode: ' . $e->getMessage());
        }

        return redirect()
            ->route('admin.settings.index', ['category' => 'updates'])
            ->with('success', 'Update cancelled and site brought out of maintenance mode.');
    }

    public function requestUpdateDownload(Request $request)
    {
        $baseUrl = (string) config('services.update_server.base_url', Setting::get('update_api_base_url'));
        $licenseKey = $request->input('update_license_key');
        if (!is_string($licenseKey) || trim($licenseKey) === '') {
            $licenseKey = Setting::get('update_license_key');
        }
        $productSecret = (string) config('services.update_server.product_secret', Setting::get('update_product_secret'));
        $productName = (string) config('services.update_server.product_name', Setting::get('update_product_name'));

        $missing = [];

        if (!is_string($baseUrl) || trim($baseUrl) === '') {
            $missing[] = 'Update API Base URL';
        }
        if (!is_string($licenseKey) || trim($licenseKey) === '') {
            $missing[] = 'License Key';
        }
        if (!is_string($productSecret) || trim($productSecret) === '') {
            $missing[] = 'Product Secret';
        }
        if (!is_string($productName) || trim($productName) === '') {
            $missing[] = 'Product Name';
        }

        if (!empty($missing)) {
            return redirect()
                ->route('admin.settings.index', ['category' => 'updates'])
                ->with('error', 'Missing required update settings: ' . implode(', ', $missing) . '.');
        }

        $licenseKey = $this->normalizePurchaseCode(is_string($licenseKey) ? $licenseKey : null);
        if ($licenseKey === '' || !preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $licenseKey)) {
            return redirect()
                ->route('admin.settings.index', ['category' => 'updates'])
                ->with('error', 'Invalid purchase code format. Please use the Envato/ThemeForest purchase code (e.g. xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).');
        }

        $appUrl = (string) config('app.url');
        $parsed = parse_url($appUrl);
        $domain = is_array($parsed) && is_string($parsed['host'] ?? null) && trim((string) $parsed['host']) !== ''
            ? (string) $parsed['host']
            : $appUrl;

        $licenseCheck = $this->updateServerService->licenseCheck(
            (string) $licenseKey,
            $domain,
            (string) $productSecret,
            (string) $productName
        );

        if (!($licenseCheck['valid'] ?? false)) {
            $licenseActivate = $this->updateServerService->licenseActivate(
                (string) $licenseKey,
                $domain,
                (string) $productSecret,
                (string) $productName
            );

            if (!($licenseActivate['valid'] ?? false)) {
                $message = is_string($licenseActivate['message'] ?? null) && trim((string) $licenseActivate['message']) !== ''
                    ? (string) $licenseActivate['message']
                    : (is_string($licenseCheck['message'] ?? null) && trim((string) $licenseCheck['message']) !== ''
                        ? (string) $licenseCheck['message']
                        : 'License key is not valid.');

                return redirect()
                    ->route('admin.settings.index', ['category' => 'updates'])
                    ->with('error', $message);
            }
        }

        $result = $this->updateServerService->requestDownloadUrl(
            (string) $baseUrl,
            (string) $licenseKey,
            $domain,
            (string) $productSecret,
            (string) $productName
        );

        if (!($result['success'] ?? false)) {
            $message = is_string($result['message'] ?? null) && trim((string) $result['message']) !== ''
                ? (string) $result['message']
                : 'Failed to get download link.';

            if (is_numeric($result['status'] ?? null)) {
                $message .= ' (HTTP ' . (int) $result['status'] . ')';
            }
            if (array_key_exists('raw', $result) && $result['raw'] !== null) {
                $raw = is_string($result['raw']) ? $result['raw'] : json_encode($result['raw']);
                if (is_string($raw) && trim($raw) !== '') {
                    $message .= ': ' . $raw;
                }
            }

            return redirect()
                ->route('admin.settings.index', ['category' => 'updates'])
                ->with('error', $message);
        }

        $downloadUrl = is_string($result['download_url'] ?? null) ? $result['download_url'] : null;

        if (!is_string($downloadUrl) || trim($downloadUrl) === '') {
            return redirect()
                ->route('admin.settings.index', ['category' => 'updates'])
                ->with('error', 'Download URL not available.');
        }

        return redirect()
            ->route('admin.settings.index', ['category' => 'updates'])
            ->with('success', 'Download link generated.')
            ->with('update_download_url', $downloadUrl);
    }

    public function activateLicense(Request $request)
    {
        $baseUrl = (string) config('services.update_server.base_url', Setting::get('update_api_base_url'));
        $licenseKey = $request->input('update_license_key');
        if (!is_string($licenseKey) || trim($licenseKey) === '') {
            $licenseKey = Setting::get('update_license_key');
        }
        $productSecret = (string) config('services.update_server.product_secret', Setting::get('update_product_secret'));
        $productName = (string) config('services.update_server.product_name', Setting::get('update_product_name'));

        $missing = [];

        if (!is_string($licenseKey) || trim($licenseKey) === '') {
            $missing[] = 'License Key';
        }
        if (!is_string($productSecret) || trim($productSecret) === '') {
            $missing[] = 'Product Secret';
        }

        if (!empty($missing)) {
            return redirect()
                ->route('admin.settings.index', ['category' => 'updates'])
                ->with('error', 'Missing required settings: ' . implode(', ', $missing) . '.');
        }

        $licenseKey = $this->normalizePurchaseCode(is_string($licenseKey) ? $licenseKey : null);
        if ($licenseKey === '' || !preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $licenseKey)) {
            return redirect()
                ->route('admin.settings.index', ['category' => 'updates'])
                ->with('error', 'Invalid purchase code format. Please use the Envato/ThemeForest purchase code (e.g. xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).');
        }

        $appUrl = (string) config('app.url');
        $parsed = parse_url($appUrl);
        $domain = is_array($parsed) && is_string($parsed['host'] ?? null) && trim((string) $parsed['host']) !== ''
            ? (string) $parsed['host']
            : $appUrl;

        $result = $this->updateServerService->licenseActivate(
            (string) $licenseKey,
            $domain,
            (string) $productSecret,
            (string) $productName
        );

        $cacheKey = 'update_server:license_check:' . md5((string) $baseUrl) . ':' . md5((string) $licenseKey) . ':' . md5((string) $domain);
        Cache::forget($cacheKey);

        if (!($result['valid'] ?? false)) {
            $message = is_string($result['message'] ?? null) && trim((string) $result['message']) !== ''
                ? (string) $result['message']
                : 'Failed to activate license.';

            return redirect()
                ->route('admin.settings.index', ['category' => 'updates'])
                ->with('error', $message);
        }

        Setting::set('update_license_key', $licenseKey);

        $message = null;
        if (is_array($result['data'] ?? null) && is_string($result['data']['message'] ?? null)) {
            $message = trim((string) $result['data']['message']);
        }
        if (!is_string($message) || $message === '') {
            $message = 'License activated successfully.';
        }

        return redirect()
            ->route('admin.settings.index', ['category' => 'updates', 'refresh' => 1])
            ->with('success', $message);
    }

    public function deactivateLicense(Request $request)
    {
        $baseUrl = (string) config('services.update_server.base_url', Setting::get('update_api_base_url'));
        $licenseKey = Setting::get('update_license_key');
        $licenseKey = is_string($licenseKey) ? trim((string) $licenseKey) : '';
        $productSecret = (string) config('services.update_server.product_secret', Setting::get('update_product_secret'));
        $productName = (string) config('services.update_server.product_name', Setting::get('update_product_name'));

        $missing = [];

        if (!is_string($baseUrl) || trim($baseUrl) === '') {
            $missing[] = 'Update API Base URL';
        }
        if (!is_string($licenseKey) || trim($licenseKey) === '') {
            $missing[] = 'License Key';
        }
        if (!is_string($productSecret) || trim($productSecret) === '') {
            $missing[] = 'Product Secret';
        }
        if (!is_string($productName) || trim($productName) === '') {
            $missing[] = 'Product Name';
        }

        if (!empty($missing)) {
            return redirect()
                ->route('admin.settings.index', ['category' => 'updates'])
                ->with('error', 'Missing required update settings: ' . implode(', ', $missing) . '.');
        }

        $licenseKey = $this->normalizePurchaseCode(is_string($licenseKey) ? $licenseKey : null);
        if ($licenseKey === '' || !preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $licenseKey)) {
            return redirect()
                ->route('admin.settings.index', ['category' => 'updates'])
                ->with('error', 'Invalid purchase code format. Please use the Envato/ThemeForest purchase code (e.g. xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).');
        }

        $appUrl = (string) config('app.url');
        $parsed = parse_url($appUrl);
        $domain = is_array($parsed) && is_string($parsed['host'] ?? null) && trim((string) $parsed['host']) !== ''
            ? (string) $parsed['host']
            : $appUrl;

        $result = $this->updateServerService->licenseDeactivate(
            (string) $licenseKey,
            $domain,
            (string) $productSecret,
            (string) $productName
        );

        $cacheKey = 'update_server:license_check:' . md5((string) $baseUrl) . ':' . md5((string) $licenseKey) . ':' . md5((string) $domain);
        Cache::forget($cacheKey);

        if (!($result['success'] ?? false) && !($result['valid'] ?? false)) {
            $data = $result['data'] ?? null;
            $status = is_array($data) ? ($data['status'] ?? null) : null;
            if (!is_string($status) || !in_array(strtolower(trim($status)), ['success', 'deactivated'], true)) {
                $message = is_string($result['message'] ?? null) && trim((string) $result['message']) !== ''
                    ? (string) $result['message']
                    : 'Failed to deactivate license.';

                return redirect()
                    ->route('admin.settings.index', ['category' => 'updates'])
                    ->with('error', $message);
            }
        }

        Setting::set('update_license_key', null);

        $message = null;
        if (is_array($result['data'] ?? null) && is_string($result['data']['message'] ?? null)) {
            $message = trim((string) $result['data']['message']);
        }
        if (!is_string($message) || $message === '') {
            $message = 'License deactivated.';
        }

        return redirect()
            ->route('admin.settings.index', ['category' => 'updates', 'refresh' => 1])
            ->with('success', $message);
    }

    public function clearApplicationCache(Request $request)
    {
        $commands = [
            'optimize:clear',
            'view:clear',
            'config:clear',
            'route:clear',
            'cache:clear',
        ];

        $errors = [];

        foreach ($commands as $command) {
            try {
                Artisan::call($command);
            } catch (\Throwable $e) {
                $errors[] = $command . ': ' . $e->getMessage();
            }
        }

        $opcacheResetAttempted = false;
        $opcacheResetSucceeded = false;

        try {
            if (function_exists('opcache_reset')) {
                $opcacheResetAttempted = true;
                $opcacheResetSucceeded = (bool) opcache_reset();
            }
        } catch (\Throwable $e) {
            $errors[] = 'opcache_reset: ' . $e->getMessage();
        }

        if (!empty($errors)) {
            return redirect()
                ->route('admin.settings.index', ['category' => 'updates'])
                ->with('error', 'Some cache refresh steps failed: ' . implode(' | ', $errors));
        }

        $message = 'Application cache cleared successfully.';

        if ($opcacheResetAttempted) {
            $message .= $opcacheResetSucceeded
                ? ' PHP OPcache was also reset.'
                : ' PHP OPcache reset was attempted but not confirmed.';
        }

        return redirect()
            ->route('admin.settings.index', ['category' => 'updates'])
            ->with('success', $message);
    }

    /**
     * Update settings.
     */
    public function update(SettingUpdateRequest $request)
    {
        $category = $request->input('category', 'general');
        $categories = $this->settingService->getCategories();

        $categories = array_values(array_filter($categories, fn ($cat) => !in_array($cat, ['homepage', 'templates', 'addon', 'addons', 'site_pages', 'unibox'], true)));

        $brandingDisk = (string) config('filesystems.branding_disk', 'public');

        if (!in_array($category, $categories, true)) {
            $category = $categories[0] ?? 'general';
        }
        $data = $request->except(['_token', '_method', 'category']);

        if ($category === 'billing') {
            foreach ($this->billingKeysToHide() as $key) {
                unset($data[$key]);
            }
        }

        foreach (['app_logo', 'app_logo_dark', 'site_favicon', 'public_meta_image'] as $imageKey) {
            $removeFlag = filter_var($request->input('remove_' . $imageKey), FILTER_VALIDATE_BOOLEAN);

            if (!$removeFlag) {
                continue;
            }

            $oldPath = Setting::get($imageKey);
            if (is_string($oldPath) && $oldPath !== '') {
                Storage::disk($brandingDisk)->delete($oldPath);
            }

            $data[$imageKey] = null;
            unset($data['remove_' . $imageKey]);
        }

        if ($request->hasFile('app_logo')) {
            $file = $request->file('app_logo');

            if ($file && $file->isValid()) {
                $oldPath = Setting::get('app_logo');
                if (is_string($oldPath) && $oldPath !== '') {
                    Storage::disk($brandingDisk)->delete($oldPath);
                }

                $path = $file->storePublicly('branding', $brandingDisk);
                $data['app_logo'] = $path;
            }
        }

        if ($request->hasFile('app_logo_dark')) {
            $file = $request->file('app_logo_dark');

            if ($file && $file->isValid()) {
                $oldPath = Setting::get('app_logo_dark');
                if (is_string($oldPath) && $oldPath !== '') {
                    Storage::disk($brandingDisk)->delete($oldPath);
                }

                $path = $file->storePublicly('branding', $brandingDisk);
                $data['app_logo_dark'] = $path;
            }
        }

        if ($request->hasFile('site_favicon')) {
            $file = $request->file('site_favicon');

            if ($file && $file->isValid()) {
                $oldPath = Setting::get('site_favicon');
                if (is_string($oldPath) && $oldPath !== '') {
                    Storage::disk($brandingDisk)->delete($oldPath);
                }

                $path = $file->storePublicly('branding', $brandingDisk);
                $data['site_favicon'] = $path;
            }
        }

        if ($request->hasFile('public_meta_image')) {
            $file = $request->file('public_meta_image');

            if ($file && $file->isValid()) {
                $oldPath = Setting::get('public_meta_image');
                if (is_string($oldPath) && $oldPath !== '') {
                    Storage::disk($brandingDisk)->delete($oldPath);
                }

                $path = $file->storePublicly('branding', $brandingDisk);
                $data['public_meta_image'] = $path;
            }
        }

        $booleanKeys = Setting::query()
            ->where('type', 'boolean')
            ->where('category', $category)
            ->pluck('key');

        if ($category === 'general') {
            $booleanKeys = $booleanKeys->merge(['home_redirect_enabled'])->unique()->values();
        }

        foreach ($booleanKeys as $key) {
            if (!array_key_exists($key, $data)) {
                $data[$key] = 0;
            }
        }

        foreach (['google_client_secret', 's3_secret', 'wasabi_secret', 'update_product_secret', 'update_license_key', 'openai_api_key', 'gemini_api_key'] as $secretKey) {
            if (!array_key_exists($secretKey, $data)) {
                continue;
            }

            $secret = $data[$secretKey];
            if (!is_string($secret) || trim($secret) === '' || trim($secret) === '********') {
                unset($data[$secretKey]);
            }
        }

        if ($category === 'updates' && array_key_exists('update_license_key', $data)) {
            $licenseKey = $data['update_license_key'];

            if (is_string($licenseKey) && trim($licenseKey) !== '') {
                $licenseKey = $this->normalizePurchaseCode($licenseKey);
                if ($licenseKey === '' || !preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $licenseKey)) {
                    return redirect()
                        ->route('admin.settings.index', ['category' => $category])
                        ->with('error', 'Invalid purchase code format. Please use the Envato/ThemeForest purchase code (e.g. xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).');
                }

                $data['update_license_key'] = $licenseKey;
            }
        }

        if (array_key_exists('site_language', $data)) {
            Cache::forget('translation_locales:site');
        }

        if (array_key_exists('home_page_title', $data)) {
            $homePageTitle = $data['home_page_title'];
            $homePageTitle = is_string($homePageTitle) ? trim($homePageTitle) : '';
            if ($homePageTitle === '') {
                $homePageTitle = 'Self-Hosted Email Marketing Platform';
            }

            Setting::set('home_page_title', $homePageTitle, 'general', 'string');
            unset($data['home_page_title']);
        }

        $this->settingService->updateSettings($data);

        return redirect()
            ->route('admin.settings.index', ['category' => $category])
            ->with('success', 'Settings updated successfully.');
    }

    public function revealSecret(Request $request, string $key)
    {
        $allowed = [
            'google_client_secret',
            's3_secret',
            'wasabi_secret',
            'update_product_secret',
            'update_license_key',
            'openai_api_key',
            'gemini_api_key',
        ];

        if (!in_array($key, $allowed, true)) {
            abort(404);
        }

        $value = Setting::get($key);
        $value = is_string($value) ? trim($value) : '';

        return response()->json([
            'success' => true,
            'value' => $value,
        ]);
    }

    public function activateTemplate(Request $request, string $template)
    {
        $tpl = $this->getTemplateOrAbort($template);
        $this->ensureTemplateUnlocked($tpl);

        $variant = is_string($tpl['variant'] ?? null) ? trim((string) $tpl['variant']) : '1';
        if ($variant === '' || !in_array($variant, ['1', '2', '3', '4', '5'], true)) {
            $variant = '1';
        }

        Setting::set('home_page_variant', $variant, 'general', 'string');
        Setting::set('active_public_template', (string) ($tpl['id'] ?? $template), 'templates', 'string');

        $values = $this->loadTemplateValues($template);
        if (is_string($values['brand_color'] ?? null)) {
            Setting::set('brand_color', (string) $values['brand_color'], 'appearance', 'string');
        }

        if ($request->expectsJson()) {
            return response()->json([
                'success' => true,
                'template_id' => (string) ($tpl['id'] ?? $template),
                'variant' => $variant,
            ]);
        }

        return redirect()
            ->route('admin.settings.index', ['category' => 'templates'])
            ->with('success', 'Template activated.');
    }

    public function editTemplate(Request $request, string $template)
    {
        $tpl = $this->getTemplateOrAbort($template);
        $this->ensureTemplateUnlocked($tpl);

        $values = $this->loadTemplateValues($template);

        return view('admin.settings.template-editor', [
            'templateId' => $template,
            'template' => $tpl,
            'values' => $values,
        ]);
    }

    public function templateValues(Request $request, string $template): JsonResponse
    {
        $tpl = $this->getTemplateOrAbort($template);
        $this->ensureTemplateUnlocked($tpl);

        $values = $this->loadTemplateValues($template);

        return response()->json([
            'template_id' => (string) ($tpl['id'] ?? $template),
            'template' => $tpl,
            'editable_keys' => $this->templateEditableKeys($template),
            'values' => $values,
        ]);
    }

    public function updateTemplate(Request $request, string $template)
    {
        $tpl = $this->getTemplateOrAbort($template);
        $this->ensureTemplateUnlocked($tpl);

        $normalized = $request->all();
        foreach ($this->templateEditableKeys($template) as $key) {
            if (!array_key_exists($key, $normalized)) {
                continue;
            }
            if (is_string($normalized[$key]) && trim($normalized[$key]) === '') {
                $normalized[$key] = null;
            }
        }
        $request->replace($normalized);

        $data = $request->validate($this->templateValidationRules($template));

        foreach ($this->templateEditableKeys($template) as $key) {
            if (!array_key_exists($key, $data)) {
                continue;
            }
            $val = $data[$key];
            $settingKey = $this->templateSettingKey($template, $key);
            Setting::set($settingKey, $val, 'templates', 'string');
        }

        Setting::set('active_public_template', (string) ($tpl['id'] ?? $template), 'templates', 'string');

        try {
            $activeVariant = Setting::get('home_page_variant', '1');
        } catch (\Throwable $e) {
            $activeVariant = '1';
        }
        $activeVariant = is_string($activeVariant) ? trim($activeVariant) : '1';

        if ((string) ($tpl['variant'] ?? '') === (string) $activeVariant && is_string($data['brand_color'] ?? null) && $data['brand_color'] !== '') {
            Setting::set('brand_color', (string) $data['brand_color'], 'appearance', 'string');
        }

        if ($request->expectsJson()) {
            $values = $this->loadTemplateValues($template);
            return response()->json([
                'success' => true,
                'template_id' => (string) ($tpl['id'] ?? $template),
                'values' => $values,
            ]);
        }

        return redirect()
            ->route('admin.settings.templates.edit', ['template' => $template])
            ->with('success', 'Template updated.');
    }

    public function previewTemplate(Request $request, string $template)
    {
        $tpl = $this->getTemplateOrAbort($template);

        $normalized = $request->all();
        foreach ($this->templateEditableKeys($template) as $key) {
            if (!array_key_exists($key, $normalized)) {
                continue;
            }
            if (is_string($normalized[$key]) && trim($normalized[$key]) === '') {
                $normalized[$key] = null;
            }
        }
        $request->replace($normalized);

        $validated = $request->validate($this->templateValidationRules($template));
        $values = $this->loadTemplateValues($template, $validated);

        $variant = is_string($tpl['variant'] ?? null) ? trim((string) $tpl['variant']) : '1';
        if ($variant === '' || !in_array($variant, ['1', '2', '3', '4', '5'], true)) {
            $variant = '1';
        }

        $view = match ($variant) {
            '2' => 'public.home-2',
            '3' => 'public.home-3',
            '4' => 'public.home-v2',
            '5' => 'public.home-5',
            default => 'public.home',
        };

        $payload = [];
        if ($variant === '5') {
            $payload['templateOverrides'] = $values;
        }
        if (is_string($values['brand_color'] ?? null) && preg_match('/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/', (string) $values['brand_color'])) {
            $payload['brandColorOverride'] = (string) $values['brand_color'];
        }

        $html = view($view, $payload)->render();
        return response($html);
    }

    private function externalTemplatesConfig(Request $request): array
    {
        $baseUrl = $request->input('external_templates_api_base_url', Setting::get('external_templates_api_base_url', ''));
        $productId = $request->input('external_templates_api_product_id', Setting::get('external_templates_api_product_id', ''));
        $resourceType = $request->input('external_templates_api_resource_type', Setting::get('external_templates_api_resource_type', 'Post'));

        $baseUrl = is_string($baseUrl) ? trim($baseUrl) : '';
        $productId = is_string($productId) ? trim($productId) : (is_numeric($productId) ? (string) $productId : '');
        $resourceType = is_string($resourceType) ? trim($resourceType) : '';

        return [
            'base_url' => rtrim($baseUrl, '/'),
            'product_id' => $productId,
            'resource_type' => $resourceType,
        ];
    }

    public function syncExternalTemplates(Request $request)
    {
        $cfg = $this->externalTemplatesConfig($request);

        $baseUrl = $cfg['base_url'];
        $productId = $cfg['product_id'];
        $resourceType = $cfg['resource_type'];

        if ($baseUrl !== '' && filter_var($baseUrl, FILTER_VALIDATE_URL)) {
            Setting::set('external_templates_api_base_url', $baseUrl, 'templates', 'string');
        }
        if ($productId !== '') {
            Setting::set('external_templates_api_product_id', $productId, 'templates', 'string');
        }
        if ($resourceType !== '') {
            Setting::set('external_templates_api_resource_type', $resourceType, 'templates', 'string');
        }

        if ($baseUrl === '' || !filter_var($baseUrl, FILTER_VALIDATE_URL)) {
            $msg = 'External templates API base URL is missing or invalid.';
            if ($request->expectsJson()) {
                return response()->json(['success' => false, 'message' => $msg], 422);
            }
            return redirect()->route('admin.settings.index', ['category' => 'templates'])->with('error', $msg);
        }
        if ($resourceType === '') {
            $msg = 'External templates resource type is required.';
            if ($request->expectsJson()) {
                return response()->json(['success' => false, 'message' => $msg], 422);
            }
            return redirect()->route('admin.settings.index', ['category' => 'templates'])->with('error', $msg);
        }
        if ($productId === '' || !ctype_digit($productId)) {
            $msg = 'External templates product id is required.';
            if ($request->expectsJson()) {
                return response()->json(['success' => false, 'message' => $msg], 422);
            }
            return redirect()->route('admin.settings.index', ['category' => 'templates'])->with('error', $msg);
        }

        $url = $baseUrl . '/wp-json/v1/templates/' . rawurlencode($resourceType);

        try {
            $resp = Http::timeout(20)->acceptJson()->get($url, [
                'product_id' => (int) $productId,
            ]);
        } catch (\Throwable $e) {
            Log::warning('External templates sync failed: ' . $e->getMessage());
            $msg = 'Failed to reach external templates API.';
            if ($request->expectsJson()) {
                return response()->json(['success' => false, 'message' => $msg], 502);
            }
            return redirect()->route('admin.settings.index', ['category' => 'templates'])->with('error', $msg);
        }

        if (!$resp->successful()) {
            $msg = 'External templates API returned an error.';
            if ($request->expectsJson()) {
                return response()->json(['success' => false, 'message' => $msg], 502);
            }
            return redirect()->route('admin.settings.index', ['category' => 'templates'])->with('error', $msg);
        }

        $payload = $resp->json();
        $templates = is_array($payload) && is_array($payload['templates'] ?? null) ? $payload['templates'] : [];

        $synced = 0;
        foreach ($templates as $t) {
            if (!is_array($t)) {
                continue;
            }

            $externalId = $t['id'] ?? null;
            if (!is_int($externalId) && !(is_string($externalId) && ctype_digit($externalId))) {
                continue;
            }
            $externalId = (int) $externalId;

            $meta = is_array($t['meta'] ?? null) ? $t['meta'] : [];
            $requiresLicense = false;
            if (array_key_exists('requires_license', $meta)) {
                $requiresLicense = (string) $meta['requires_license'] === '1' || $meta['requires_license'] === 1 || $meta['requires_license'] === true;
            }

            $createdAt = is_string($t['created_at'] ?? null) ? $t['created_at'] : null;
            $updatedAt = is_string($t['updated_at'] ?? null) ? $t['updated_at'] : null;

            ExternalTemplate::updateOrCreate(
                ['external_id' => $externalId],
                [
                    'name' => is_string($t['name'] ?? null) ? $t['name'] : null,
                    'resource_type' => is_string($payload['resource_type'] ?? null) ? $payload['resource_type'] : $resourceType,
                    'template_type' => is_string($t['type'] ?? null) ? $t['type'] : null,
                    'product_id' => is_int($t['product_id'] ?? null) ? $t['product_id'] : (is_string($t['product_id'] ?? null) && ctype_digit($t['product_id']) ? (int) $t['product_id'] : (int) $productId),
                    'preview_image' => is_string($t['preview_image'] ?? null) ? $t['preview_image'] : null,
                    'preview_url' => is_string($t['preview_url'] ?? null) ? $t['preview_url'] : null,
                    'requires_license' => $requiresLicense,
                    'plan' => is_string($meta['plan'] ?? null) ? $meta['plan'] : null,
                    'builder' => is_string($meta['builder'] ?? null) ? $meta['builder'] : null,
                    'categories' => is_array($t['categories'] ?? null) ? $t['categories'] : null,
                    'meta' => $meta,
                    'external_created_at' => is_string($createdAt) ? Carbon::parse($createdAt) : null,
                    'external_updated_at' => is_string($updatedAt) ? Carbon::parse($updatedAt) : null,
                ]
            );

            $synced++;
        }

        if ($request->expectsJson()) {
            return response()->json(['success' => true, 'synced' => $synced]);
        }

        return redirect()->route('admin.settings.index', ['category' => 'templates'])->with('success', 'Synced external templates: ' . $synced);
    }

    public function fetchExternalTemplateJson(Request $request, string $externalId)
    {
        $tpl = ExternalTemplate::query()->where('external_id', (int) $externalId)->first();
        if (!$tpl) {
            abort(404);
        }

        $licenseActive = (bool) Setting::get('external_templates_license_active', 0);
        if ($tpl->requires_license && !$licenseActive) {
            $msg = 'This template requires an active license.';
            if ($request->expectsJson()) {
                return response()->json(['success' => false, 'message' => $msg], 403);
            }
            return redirect()->route('admin.settings.index', ['category' => 'templates'])->with('error', $msg);
        }

        $baseUrl = (string) Setting::get('external_templates_api_base_url', '');
        $baseUrl = trim($baseUrl);
        $baseUrl = rtrim($baseUrl, '/');

        if ($baseUrl === '' || !filter_var($baseUrl, FILTER_VALIDATE_URL)) {
            $msg = 'External templates API base URL is missing or invalid.';
            if ($request->expectsJson()) {
                return response()->json(['success' => false, 'message' => $msg], 422);
            }
            return redirect()->route('admin.settings.index', ['category' => 'templates'])->with('error', $msg);
        }

        $url = $baseUrl . '/wp-json/v1/templates/' . rawurlencode((string) $tpl->external_id);

        try {
            $resp = Http::timeout(25)->acceptJson()->post($url, []);
        } catch (\Throwable $e) {
            Log::warning('External template json fetch failed: ' . $e->getMessage());
            $msg = 'Failed to fetch external template JSON.';
            if ($request->expectsJson()) {
                return response()->json(['success' => false, 'message' => $msg], 502);
            }
            return redirect()->route('admin.settings.index', ['category' => 'templates'])->with('error', $msg);
        }

        if (!$resp->successful()) {
            $msg = 'External templates API returned an error.';
            if ($request->expectsJson()) {
                return response()->json(['success' => false, 'message' => $msg], 502);
            }
            return redirect()->route('admin.settings.index', ['category' => 'templates'])->with('error', $msg);
        }

        $payload = $resp->json();
        $jsonCode = is_array($payload) ? ($payload['json_code'] ?? null) : null;

        if (!is_string($jsonCode) || trim($jsonCode) === '') {
            $msg = 'External templates API did not return json_code.';
            if ($request->expectsJson()) {
                return response()->json(['success' => false, 'message' => $msg], 422);
            }
            return redirect()->route('admin.settings.index', ['category' => 'templates'])->with('error', $msg);
        }

        $tpl->json_code = $jsonCode;
        $tpl->json_fetched_at = now();
        $tpl->save();

        if ($request->expectsJson()) {
            return response()->json(['success' => true]);
        }

        return redirect()->route('admin.settings.index', ['category' => 'templates'])->with('success', 'Fetched template JSON.');
    }

    public function activateExternalTemplatesLicense(Request $request)
    {
        $licenseKey = $request->input('license_key');
        $licenseKey = is_string($licenseKey) ? trim($licenseKey) : '';

        if ($licenseKey === '') {
            $msg = 'License key is required.';
            if ($request->expectsJson()) {
                return response()->json(['success' => false, 'message' => $msg], 422);
            }
            return redirect()->route('admin.settings.index', ['category' => 'templates'])->with('error', $msg);
        }

        Setting::set('external_templates_license_key', $licenseKey, 'templates', 'string');
        Setting::set('external_templates_license_active', 1, 'templates', 'boolean');

        if ($request->expectsJson()) {
            return response()->json(['success' => true]);
        }

        return redirect()->route('admin.settings.index', ['category' => 'templates'])->with('success', 'License activated.');
    }

    public function deactivateExternalTemplatesLicense(Request $request)
    {
        Setting::set('external_templates_license_active', 0, 'templates', 'boolean');

        if ($request->expectsJson()) {
            return response()->json(['success' => true]);
        }

        return redirect()->route('admin.settings.index', ['category' => 'templates'])->with('success', 'License deactivated.');
    }
}